diff options
| author | 2017-05-05 14:06:23 +0000 | |
|---|---|---|
| committer | 2017-05-05 14:06:23 +0000 | |
| commit | cba224e83385c5141c9dfbf9f8cfee2d7fc83f31 (patch) | |
| tree | b51a39272f2cd2c32f503e9e4490d7d0e744cd94 | |
| parent | a34610cebc0a4535ed495adecfe56d37ce66bbaf (diff) | |
| parent | 68f666693a465eb8a66d9252b7b7ac035b9f0b7b (diff) | |
Merge "Add (disabled) time zone update system server impl"
25 files changed, 4824 insertions, 0 deletions
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index ce019cac3772..1e5ea26efcc7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -26,6 +26,7 @@ import android.accounts.IAccountManager; import android.app.admin.DevicePolicyManager; import android.app.job.IJobScheduler; import android.app.job.JobScheduler; +import android.app.timezone.RulesManager; import android.app.trust.TrustManager; import android.app.usage.IUsageStatsManager; import android.app.usage.NetworkStatsManager; @@ -786,6 +787,13 @@ final class SystemServiceRegistry { return new ContextHubManager(ctx.getOuterContext(), ctx.mMainThread.getHandler().getLooper()); }}); + + registerService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, RulesManager.class, + new CachedServiceFetcher<RulesManager>() { + @Override + public RulesManager createService(ContextImpl ctx) { + return new RulesManager(ctx.getOuterContext()); + }}); } /** diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 589aa07d7f98..22a1a3663342 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1282,6 +1282,49 @@ <!-- True if WallpaperService is enabled --> <bool name="config_enableWallpaperService">true</bool> + <!-- Enables the TimeZoneRuleManager service. This is the master switch for the updateable time + zone update mechanism. --> + <bool name="config_enableUpdateableTimeZoneRules">false</bool> + + <!-- Enables APK-based time zone update triggering. Set this to false when updates are triggered + via external events and not by APK updates. For example, if an updater checks with a server + on a regular schedule. + [This is only used if config_enableUpdateableTimeZoneRules is true.] --> + <bool name="config_timeZoneRulesUpdateTrackingEnabled">false</bool> + + <!-- The package of the time zone rules updater application. Expected to be the same + for all Android devices that support APK-based time zone rule updates. + A package-targeted android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK intent + will be sent to the updater app if the system server detects an update to the updater or + data app packages. + The package referenced here must have the android.permission.UPDATE_TIME_ZONE_RULES + permission. + [This is only used if config_enableUpdateableTimeZoneRules and + config_timeZoneRulesUpdateTrackingEnabled are true.] --> + <string name="config_timeZoneRulesUpdaterPackage" translateable="false"></string> + + <!-- The package of the time zone rules data application. Expected to be configured + by OEMs to reference their own priv-app APK package. + A package-targeted android.intent.action.timezone.TRIGGER_RULES_UPDATE_CHECK intent + will be sent to the updater app if the system server detects an update to the updater or + data app packages. + [This is only used if config_enableUpdateableTimeZoneRules and + config_timeZoneRulesUpdateTrackingEnabled are true.] --> + <string name="config_timeZoneRulesDataPackage" translateable="false"></string> + + <!-- The allowed time in milliseconds between an update check intent being broadcast and the + response being considered overdue. Reliability triggers will not fire in this time. + [This is only used if config_enableUpdateableTimeZoneRules and + config_timeZoneRulesUpdateTrackingEnabled are true.] --> + <!-- 5 minutes --> + <integer name="config_timeZoneRulesCheckTimeMillisAllowed">300000</integer> + + <!-- The number of times a time zone update check is allowed to fail before the system will stop + reacting to reliability triggers. + [This is only used if config_enableUpdateableTimeZoneRules and + config_timeZoneRulesUpdateTrackingEnabled are true.] --> + <integer name="config_timeZoneRulesCheckRetryCount">5</integer> + <!-- Whether to enable network location overlay which allows network location provider to be replaced by an app at run-time. When disabled, only the config_networkLocationProviderPackageName package will be diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 99dc9b4e813f..ea89cc15c0b0 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -275,6 +275,12 @@ <java-symbol type="bool" name="split_action_bar_is_narrow" /> <java-symbol type="bool" name="config_useVolumeKeySounds" /> <java-symbol type="bool" name="config_enableWallpaperService" /> + <java-symbol type="bool" name="config_enableUpdateableTimeZoneRules" /> + <java-symbol type="bool" name="config_timeZoneRulesUpdateTrackingEnabled" /> + <java-symbol type="string" name="config_timeZoneRulesUpdaterPackage" /> + <java-symbol type="string" name="config_timeZoneRulesDataPackage" /> + <java-symbol type="integer" name="config_timeZoneRulesCheckTimeMillisAllowed" /> + <java-symbol type="integer" name="config_timeZoneRulesCheckRetryCount" /> <java-symbol type="bool" name="config_sendAudioBecomingNoisy" /> <java-symbol type="bool" name="config_enableScreenshotChord" /> <java-symbol type="bool" name="config_bluetooth_default_profiles" /> diff --git a/services/core/java/com/android/server/timezone/CheckToken.java b/services/core/java/com/android/server/timezone/CheckToken.java new file mode 100644 index 000000000000..51283608c66e --- /dev/null +++ b/services/core/java/com/android/server/timezone/CheckToken.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +/** + * A deserialized version of the byte[] sent to the time zone update application to identify a + * triggered time zone update check. It encodes the optimistic lock ID used to detect + * concurrent checks and the minimal package versions that will have been checked. + */ +final class CheckToken { + + final int mOptimisticLockId; + final PackageVersions mPackageVersions; + + CheckToken(int optimisticLockId, PackageVersions packageVersions) { + this.mOptimisticLockId = optimisticLockId; + + if (packageVersions == null) { + throw new NullPointerException("packageVersions == null"); + } + this.mPackageVersions = packageVersions; + } + + byte[] toByteArray() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(12 /* (3 * sizeof(int)) */); + try (DataOutputStream dos = new DataOutputStream(baos)) { + dos.writeInt(mOptimisticLockId); + dos.writeInt(mPackageVersions.mUpdateAppVersion); + dos.writeInt(mPackageVersions.mDataAppVersion); + } catch (IOException e) { + throw new RuntimeException("Unable to write into a ByteArrayOutputStream", e); + } + return baos.toByteArray(); + } + + static CheckToken fromByteArray(byte[] tokenBytes) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(tokenBytes); + try (DataInputStream dis = new DataInputStream(bais)) { + int versionId = dis.readInt(); + int updateAppVersion = dis.readInt(); + int dataAppVersion = dis.readInt(); + return new CheckToken(versionId, new PackageVersions(updateAppVersion, dataAppVersion)); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CheckToken checkToken = (CheckToken) o; + + if (mOptimisticLockId != checkToken.mOptimisticLockId) { + return false; + } + return mPackageVersions.equals(checkToken.mPackageVersions); + } + + @Override + public int hashCode() { + int result = mOptimisticLockId; + result = 31 * result + mPackageVersions.hashCode(); + return result; + } + + @Override + public String toString() { + return "Token{" + + "mOptimisticLockId=" + mOptimisticLockId + + ", mPackageVersions=" + mPackageVersions + + '}'; + } +} diff --git a/services/core/java/com/android/server/timezone/ClockHelper.java b/services/core/java/com/android/server/timezone/ClockHelper.java new file mode 100644 index 000000000000..353728a15e7c --- /dev/null +++ b/services/core/java/com/android/server/timezone/ClockHelper.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +/** + * An easy-to-mock interface for obtaining a monotonically increasing time value in milliseconds. + */ +interface ClockHelper { + + long currentTimestamp(); +} diff --git a/services/core/java/com/android/server/timezone/ConfigHelper.java b/services/core/java/com/android/server/timezone/ConfigHelper.java new file mode 100644 index 000000000000..f9984fa1af5e --- /dev/null +++ b/services/core/java/com/android/server/timezone/ConfigHelper.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +/** + * An easy-to-mock interface around device config for use by {@link PackageTracker}; it is not + * possible to test various states with the real one because config is fixed in the system image. + */ +interface ConfigHelper { + + boolean isTrackingEnabled(); + + String getUpdateAppPackageName(); + + String getDataAppPackageName(); + + int getCheckTimeAllowedMillis(); + + int getFailedCheckRetryCount(); +} diff --git a/services/core/java/com/android/server/timezone/FileDescriptorHelper.java b/services/core/java/com/android/server/timezone/FileDescriptorHelper.java new file mode 100644 index 000000000000..c3b1101000b5 --- /dev/null +++ b/services/core/java/com/android/server/timezone/FileDescriptorHelper.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import android.os.ParcelFileDescriptor; + +import java.io.IOException; + +/** + * An easy-to-mock interface around use of {@link ParcelFileDescriptor} for use by + * {@link RulesManagerService}. + */ +interface FileDescriptorHelper { + + byte[] readFully(ParcelFileDescriptor parcelFileDescriptor) throws IOException; +} diff --git a/services/core/java/com/android/server/timezone/IntentHelper.java b/services/core/java/com/android/server/timezone/IntentHelper.java new file mode 100644 index 000000000000..0cb90657480e --- /dev/null +++ b/services/core/java/com/android/server/timezone/IntentHelper.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +/** + * An easy-to-mock interface around intent sending / receiving for use by {@link PackageTracker}; + * it is not possible to test various cases with the real one because of the need to simulate + * receiving and broadcasting intents. + */ +interface IntentHelper { + + void initialize(String updateAppPackageName, String dataAppPackageName, Listener listener); + + void sendTriggerUpdateCheck(CheckToken checkToken); + + void enableReliabilityTriggering(); + + void disableReliabilityTriggering(); + + interface Listener { + void triggerUpdateIfNeeded(boolean packageUpdated); + } +} diff --git a/services/core/java/com/android/server/timezone/IntentHelperImpl.java b/services/core/java/com/android/server/timezone/IntentHelperImpl.java new file mode 100644 index 000000000000..3ffbb2d61fd5 --- /dev/null +++ b/services/core/java/com/android/server/timezone/IntentHelperImpl.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import android.app.timezone.RulesUpdaterContract; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.PatternMatcher; +import android.util.Slog; + +import java.util.regex.Pattern; + +/** + * The bona fide implementation of {@link IntentHelper}. + */ +final class IntentHelperImpl implements IntentHelper { + + private final static String TAG = "timezone.IntentHelperImpl"; + + private final Context mContext; + private String mUpdaterAppPackageName; + + private boolean mReliabilityReceiverEnabled; + private Receiver mReliabilityReceiver; + + IntentHelperImpl(Context context) { + mContext = context; + } + + @Override + public void initialize( + String updaterAppPackageName, String dataAppPackageName, Listener listener) { + mUpdaterAppPackageName = updaterAppPackageName; + + // Register for events of interest. + + // The intent filter that triggers when package update events happen that indicate there may + // be work to do. + IntentFilter packageIntentFilter = new IntentFilter(); + // Either of these mean a downgrade? + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + packageIntentFilter.addDataScheme("package"); + packageIntentFilter.addDataSchemeSpecificPart( + updaterAppPackageName, PatternMatcher.PATTERN_LITERAL); + packageIntentFilter.addDataSchemeSpecificPart( + dataAppPackageName, PatternMatcher.PATTERN_LITERAL); + Receiver packageUpdateReceiver = new Receiver(listener, true /* packageUpdated */); + mContext.registerReceiver(packageUpdateReceiver, packageIntentFilter); + + // TODO(nfuller): Add more exotic intents as needed. e.g. + // packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + // Also, disabled...? + mReliabilityReceiver = new Receiver(listener, false /* packageUpdated */); + } + + /** Sends an intent to trigger an update check. */ + @Override + public void sendTriggerUpdateCheck(CheckToken checkToken) { + RulesUpdaterContract.sendBroadcast( + mContext, mUpdaterAppPackageName, checkToken.toByteArray()); + } + + @Override + public synchronized void enableReliabilityTriggering() { + if (!mReliabilityReceiverEnabled) { + // The intent filter that exists to make updates reliable in the event of failures / + // reboots. + IntentFilter reliabilityIntentFilter = new IntentFilter(); + reliabilityIntentFilter.addAction(Intent.ACTION_IDLE_MAINTENANCE_START); + mContext.registerReceiver(mReliabilityReceiver, reliabilityIntentFilter); + mReliabilityReceiverEnabled = true; + } + } + + @Override + public synchronized void disableReliabilityTriggering() { + if (mReliabilityReceiverEnabled) { + mContext.unregisterReceiver(mReliabilityReceiver); + mReliabilityReceiverEnabled = false; + } + } + + private static class Receiver extends BroadcastReceiver { + private final Listener mListener; + private final boolean mPackageUpdated; + + private Receiver(Listener listener, boolean packageUpdated) { + mListener = listener; + mPackageUpdated = packageUpdated; + } + + @Override + public void onReceive(Context context, Intent intent) { + Slog.d(TAG, "Received intent: " + intent.toString()); + mListener.triggerUpdateIfNeeded(mPackageUpdated); + } + } + +} diff --git a/services/core/java/com/android/server/timezone/PackageManagerHelper.java b/services/core/java/com/android/server/timezone/PackageManagerHelper.java new file mode 100644 index 000000000000..804941add891 --- /dev/null +++ b/services/core/java/com/android/server/timezone/PackageManagerHelper.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import android.content.Intent; +import android.content.pm.PackageManager; + +/** + * An easy-to-mock facade around PackageManager for use by {@link PackageTracker}; it is not + * possible to test various cases with the real one because of the need to simulate package versions + * and manifest configurations. + */ +interface PackageManagerHelper { + + int getInstalledPackageVersion(String packageName) + throws PackageManager.NameNotFoundException; + + boolean isPrivilegedApp(String packageName) throws PackageManager.NameNotFoundException; + + boolean usesPermission(String packageName, String requiredPermissionName) + throws PackageManager.NameNotFoundException; + + boolean contentProviderRegistered(String authority, String requiredPackageName); + + boolean receiverRegistered(Intent intent, String requiredPermissionName) + throws PackageManager.NameNotFoundException; +} diff --git a/services/core/java/com/android/server/timezone/PackageStatus.java b/services/core/java/com/android/server/timezone/PackageStatus.java new file mode 100644 index 000000000000..637909615b1b --- /dev/null +++ b/services/core/java/com/android/server/timezone/PackageStatus.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Information about the status of the time zone update / data packages that are persisted by the + * Android system. + */ +final class PackageStatus { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ CHECK_STARTED, CHECK_COMPLETED_SUCCESS, CHECK_COMPLETED_FAILURE }) + @interface CheckStatus {} + + /** A time zone update check has been started but not yet completed. */ + static final int CHECK_STARTED = 1; + /** A time zone update check has been completed and succeeded. */ + static final int CHECK_COMPLETED_SUCCESS = 2; + /** A time zone update check has been completed and failed. */ + static final int CHECK_COMPLETED_FAILURE = 3; + + @CheckStatus + final int mCheckStatus; + + // Non-null + final PackageVersions mVersions; + + PackageStatus(@CheckStatus int checkStatus, PackageVersions versions) { + this.mCheckStatus = checkStatus; + if (checkStatus < 1 || checkStatus > 3) { + throw new IllegalArgumentException("Unknown checkStatus " + checkStatus); + } + if (versions == null) { + throw new NullPointerException("versions == null"); + } + this.mVersions = versions; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PackageStatus that = (PackageStatus) o; + + if (mCheckStatus != that.mCheckStatus) { + return false; + } + return mVersions.equals(that.mVersions); + } + + @Override + public int hashCode() { + int result = mCheckStatus; + result = 31 * result + mVersions.hashCode(); + return result; + } + + @Override + public String toString() { + return "PackageStatus{" + + "mCheckStatus=" + mCheckStatus + + ", mVersions=" + mVersions + + '}'; + } +} diff --git a/services/core/java/com/android/server/timezone/PackageStatusStorage.java b/services/core/java/com/android/server/timezone/PackageStatusStorage.java new file mode 100644 index 000000000000..31f0e3145f8a --- /dev/null +++ b/services/core/java/com/android/server/timezone/PackageStatusStorage.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Slog; + +import java.io.File; + +import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE; +import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS; +import static com.android.server.timezone.PackageStatus.CHECK_STARTED; + +/** + * Storage logic for accessing/mutating the Android system's persistent state related to time zone + * update checking. There is expected to be a single instance and all methods synchronized on + * {@code this} for thread safety. + */ +final class PackageStatusStorage { + + private static final String TAG = "timezone.PackageStatusStorage"; + + private static final String DATABASE_NAME = "timezonepackagestatus.db"; + private static final int DATABASE_VERSION = 1; + + /** The table name. It will have a single row with _id == {@link #SINGLETON_ID} */ + private static final String TABLE = "status"; + private static final String COLUMN_ID = "_id"; + + /** + * Column that stores a monotonically increasing lock ID, used to detect concurrent update + * issues without on-line locks. Incremented on every write. + */ + private static final String COLUMN_OPTIMISTIC_LOCK_ID = "optimistic_lock_id"; + + /** + * Column that stores the current "check status" of the time zone update application packages. + */ + private static final String COLUMN_CHECK_STATUS = "check_status"; + + /** + * Column that stores the version of the time zone rules update application being checked / last + * checked. + */ + private static final String COLUMN_UPDATE_APP_VERSION = "update_app_package_version"; + + /** + * Column that stores the version of the time zone rules data application being checked / last + * checked. + */ + private static final String COLUMN_DATA_APP_VERSION = "data_app_package_version"; + + /** + * The ID of the one row. + */ + private static final int SINGLETON_ID = 1; + + private static final int UNKNOWN_PACKAGE_VERSION = -1; + + private final DatabaseHelper mDatabaseHelper; + + PackageStatusStorage(Context context) { + mDatabaseHelper = new DatabaseHelper(context); + } + + void deleteDatabaseForTests() { + SQLiteDatabase.deleteDatabase(mDatabaseHelper.getDatabaseFile()); + } + + /** + * Obtain the current check status of the application packages. Returns {@code null} the first + * time it is called, or after {@link #resetCheckState()}. + */ + PackageStatus getPackageStatus() { + synchronized (this) { + try { + return getPackageStatusInternal(); + } catch (IllegalArgumentException e) { + // This means that data exists in the table but it was bad. + Slog.e(TAG, "Package status invalid, resetting and retrying", e); + + // Reset the storage so it is in a good state again. + mDatabaseHelper.recoverFromBadData(); + return getPackageStatusInternal(); + } + } + } + + private PackageStatus getPackageStatusInternal() { + String[] columns = { + COLUMN_CHECK_STATUS, COLUMN_UPDATE_APP_VERSION, COLUMN_DATA_APP_VERSION + }; + Cursor cursor = mDatabaseHelper.getReadableDatabase() + .query(TABLE, columns, COLUMN_ID + " = ?", + new String[] { Integer.toString(SINGLETON_ID) }, + null /* groupBy */, null /* having */, null /* orderBy */); + if (cursor.getCount() != 1) { + Slog.e(TAG, "Unable to find package status from package status row. Rows returned: " + + cursor.getCount()); + return null; + } + cursor.moveToFirst(); + + // Determine check status. + if (cursor.isNull(0)) { + // This is normal the first time getPackageStatus() is called, or after + // resetCheckState(). + return null; + } + int checkStatus = cursor.getInt(0); + + // Determine package version. + if (cursor.isNull(1) || cursor.isNull(2)) { + Slog.e(TAG, "Package version information unexpectedly null"); + return null; + } + PackageVersions packageVersions = new PackageVersions(cursor.getInt(1), cursor.getInt(2)); + + return new PackageStatus(checkStatus, packageVersions); + } + + /** + * Generate a new {@link CheckToken} that can be passed to the time zone rules update + * application. + */ + CheckToken generateCheckToken(PackageVersions currentInstalledVersions) { + if (currentInstalledVersions == null) { + throw new NullPointerException("currentInstalledVersions == null"); + } + + synchronized (this) { + Integer optimisticLockId = getCurrentOptimisticLockId(); + if (optimisticLockId == null) { + Slog.w(TAG, "Unable to find optimistic lock ID from package status row"); + + // Recover. + optimisticLockId = mDatabaseHelper.recoverFromBadData(); + } + + int newOptimisticLockId = optimisticLockId + 1; + boolean statusRowUpdated = writeStatusRow( + optimisticLockId, newOptimisticLockId, CHECK_STARTED, currentInstalledVersions); + if (!statusRowUpdated) { + Slog.e(TAG, "Unable to update status to CHECK_STARTED in package status row." + + " synchronization failure?"); + return null; + } + return new CheckToken(newOptimisticLockId, currentInstalledVersions); + } + } + + /** + * Reset the current device state to "unknown". + */ + void resetCheckState() { + synchronized(this) { + Integer optimisticLockId = getCurrentOptimisticLockId(); + if (optimisticLockId == null) { + Slog.w(TAG, "resetCheckState: Unable to find optimistic lock ID from package" + + " status row"); + // Attempt to recover the storage state. + optimisticLockId = mDatabaseHelper.recoverFromBadData(); + } + + int newOptimisticLockId = optimisticLockId + 1; + if (!writeStatusRow(optimisticLockId, newOptimisticLockId, + null /* status */, null /* packageVersions */)) { + Slog.e(TAG, "resetCheckState: Unable to reset package status row," + + " newOptimisticLockId=" + newOptimisticLockId); + } + } + } + + /** + * Update the current device state if possible. Returns true if the update was successful. + * {@code false} indicates the storage has been changed since the {@link CheckToken} was + * generated and the update was discarded. + */ + boolean markChecked(CheckToken checkToken, boolean succeeded) { + synchronized (this) { + int optimisticLockId = checkToken.mOptimisticLockId; + int newOptimisticLockId = optimisticLockId + 1; + int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE; + return writeStatusRow(optimisticLockId, newOptimisticLockId, + status, checkToken.mPackageVersions); + } + } + + // Caller should be synchronized(this) + private Integer getCurrentOptimisticLockId() { + final String[] columns = { COLUMN_OPTIMISTIC_LOCK_ID }; + final String querySelection = COLUMN_ID + " = ?"; + final String[] querySelectionArgs = { Integer.toString(SINGLETON_ID) }; + + SQLiteDatabase database = mDatabaseHelper.getReadableDatabase(); + try (Cursor cursor = database.query(TABLE, columns, querySelection, querySelectionArgs, + null /* groupBy */, null /* having */, null /* orderBy */)) { + if (cursor.getCount() != 1) { + Slog.w(TAG, cursor.getCount() + " rows returned, expected exactly one."); + return null; + } + cursor.moveToFirst(); + return cursor.getInt(0); + } + } + + // Caller should be synchronized(this) + private boolean writeStatusRow(int optimisticLockId, int newOptimisticLockId, Integer status, + PackageVersions packageVersions) { + if ((status == null) != (packageVersions == null)) { + throw new IllegalArgumentException( + "Provide both status and packageVersions, or neither."); + } + + SQLiteDatabase database = mDatabaseHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COLUMN_OPTIMISTIC_LOCK_ID, newOptimisticLockId); + if (status == null) { + values.putNull(COLUMN_CHECK_STATUS); + values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION); + values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION); + } else { + values.put(COLUMN_CHECK_STATUS, status); + values.put(COLUMN_UPDATE_APP_VERSION, packageVersions.mUpdateAppVersion); + values.put(COLUMN_DATA_APP_VERSION, packageVersions.mDataAppVersion); + } + + String updateSelection = COLUMN_ID + " = ? AND " + COLUMN_OPTIMISTIC_LOCK_ID + " = ?"; + String[] updateSelectionArgs = { + Integer.toString(SINGLETON_ID), Integer.toString(optimisticLockId) + }; + int count = database.update(TABLE, values, updateSelection, updateSelectionArgs); + if (count > 1) { + // This has to be because of corruption: there should only ever be one row. + Slog.w(TAG, "writeStatusRow: " + count + " rows updated, expected exactly one."); + // Reset the table. + mDatabaseHelper.recoverFromBadData(); + } + + // 1 is the success case. 0 rows updated means the row is missing or the optimistic lock ID + // was not as expected, this could be because of corruption but is most likely due to an + // optimistic lock failure. Callers can decide on a case-by-case basis. + return count == 1; + } + + /** Only used during tests to force an empty table. */ + void deleteRowForTests() { + mDatabaseHelper.getWritableDatabase().delete(TABLE, null, null); + } + + /** Only used during tests to force a known table state. */ + public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) { + int optimisticLockId = getCurrentOptimisticLockId(); + writeStatusRow(optimisticLockId, optimisticLockId, checkStatus, packageVersions); + } + + static class DatabaseHelper extends SQLiteOpenHelper { + + private final Context mContext; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE + " (" + + "_id INTEGER PRIMARY KEY," + + COLUMN_OPTIMISTIC_LOCK_ID + " INTEGER NOT NULL," + + COLUMN_CHECK_STATUS + " INTEGER," + + COLUMN_UPDATE_APP_VERSION + " INTEGER NOT NULL," + + COLUMN_DATA_APP_VERSION + " INTEGER NOT NULL" + + ");"); + insertInitialRowState(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { + // no-op: nothing to upgrade + } + + /** Recover the initial data row state, returning the new current optimistic lock ID */ + int recoverFromBadData() { + // Delete the table content. + SQLiteDatabase writableDatabase = getWritableDatabase(); + writableDatabase.delete(TABLE, null /* whereClause */, null /* whereArgs */); + + // Insert the initial content. + return insertInitialRowState(writableDatabase); + } + + /** Insert the initial data row, returning the optimistic lock ID */ + private static int insertInitialRowState(SQLiteDatabase db) { + // Doesn't matter what it is, but we avoid the obvious starting value each time the row + // is reset to ensure that old tokens are unlikely to work. + final int initialOptimisticLockId = (int) System.currentTimeMillis(); + + // Insert the one row. + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, SINGLETON_ID); + values.put(COLUMN_OPTIMISTIC_LOCK_ID, initialOptimisticLockId); + values.putNull(COLUMN_CHECK_STATUS); + values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION); + values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION); + long id = db.insert(TABLE, null, values); + if (id == -1) { + Slog.w(TAG, "insertInitialRow: could not insert initial row, id=" + id); + return -1; + } + return initialOptimisticLockId; + } + + File getDatabaseFile() { + return mContext.getDatabasePath(DATABASE_NAME); + } + } +} diff --git a/services/core/java/com/android/server/timezone/PackageTracker.java b/services/core/java/com/android/server/timezone/PackageTracker.java new file mode 100644 index 000000000000..8abf7df9952b --- /dev/null +++ b/services/core/java/com/android/server/timezone/PackageTracker.java @@ -0,0 +1,504 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import com.android.internal.annotations.VisibleForTesting; + +import android.app.timezone.RulesUpdaterContract; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.TimeZoneRulesDataContract; +import android.util.Slog; + +/** + * Monitors the installed applications associated with time zone updates. If the app packages are + * updated it indicates there <em>might</em> be a time zone rules update to apply so a targeted + * broadcast intent is used to trigger the time zone updater app. + * + * <p>The "update triggering" behavior of this component can be disabled via device configuration. + * + * <p>The package tracker listens for package updates of the time zone "updater app" and "data app". + * It also listens for "reliability" triggers. Reliability triggers are there to ensure that the + * package tracker handles failures reliably and are "idle maintenance" events or something similar. + * Reliability triggers can cause a time zone update check to take place if the current state is + * unclear. For example, it can be unclear after boot or after a failure. If there are repeated + * failures reliability updates are halted until the next boot. + * + * <p>This component keeps persistent track of the most recent app packages checked to avoid + * unnecessary expense from broadcasting intents (which will cause other app processes to spawn). + * The current status is also stored to detect whether the most recently-generated check is + * complete successfully. For example, if the device was interrupted while doing a check and never + * acknowledged a check then a check will be retried the next time a "reliability trigger" event + * happens. + */ +// Also made non-final so it can be mocked. +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class PackageTracker implements IntentHelper.Listener { + private static final String TAG = "timezone.PackageTracker"; + + private final PackageManagerHelper mPackageManagerHelper; + private final IntentHelper mIntentHelper; + private final ConfigHelper mConfigHelper; + private final PackageStatusStorage mPackageStatusStorage; + private final ClockHelper mClockHelper; + + // False if tracking is disabled. + private boolean mTrackingEnabled; + + // These fields may be null if package tracking is disabled. + private String mUpdateAppPackageName; + private String mDataAppPackageName; + + // The time a triggered check is allowed to take before it is considered overdue. + private int mCheckTimeAllowedMillis; + // The number of failed checks in a row before reliability checks should stop happening. + private long mFailedCheckRetryCount; + + // Reliability check state: If a check was triggered but not acknowledged within + // mCheckTimeAllowedMillis then another one can be triggered. + private Long mLastTriggerTimestamp = null; + + // Reliability check state: Whether any checks have been triggered at all. + private boolean mCheckTriggered; + + // Reliability check state: A count of how many failures have occurred consecutively. + private int mCheckFailureCount; + + /** Creates the {@link PackageTracker} for normal use. */ + static PackageTracker create(Context context) { + PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context); + return new PackageTracker( + helperImpl /* clock */, + helperImpl /* configHelper */, + helperImpl /* packageManagerHelper */, + new PackageStatusStorage(context), + new IntentHelperImpl(context)); + } + + // A constructor that can be used by tests to supply mocked / faked dependencies. + PackageTracker(ClockHelper clockHelper, ConfigHelper configHelper, + PackageManagerHelper packageManagerHelper, PackageStatusStorage packageStatusStorage, + IntentHelper intentHelper) { + mClockHelper = clockHelper; + mConfigHelper = configHelper; + mPackageManagerHelper = packageManagerHelper; + mPackageStatusStorage = packageStatusStorage; + mIntentHelper = intentHelper; + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected synchronized void start() { + mTrackingEnabled = mConfigHelper.isTrackingEnabled(); + if (!mTrackingEnabled) { + Slog.i(TAG, "Time zone updater / data package tracking explicitly disabled."); + return; + } + + mUpdateAppPackageName = mConfigHelper.getUpdateAppPackageName(); + mDataAppPackageName = mConfigHelper.getDataAppPackageName(); + mCheckTimeAllowedMillis = mConfigHelper.getCheckTimeAllowedMillis(); + mFailedCheckRetryCount = mConfigHelper.getFailedCheckRetryCount(); + + // Validate the device configuration including the application packages. + // The manifest entries in the apps themselves are not validated until use as they can + // change and we don't want to prevent the system server starting due to a bad application. + throwIfDeviceSettingsOrAppsAreBad(); + + // Explicitly start in a reliability state where reliability triggering will do something. + mCheckTriggered = false; + mCheckFailureCount = 0; + + // Initialize the intent helper. + mIntentHelper.initialize(mUpdateAppPackageName, mDataAppPackageName, this); + + // Enable the reliability triggering so we will have at least one reliability trigger if + // a package isn't updated. + mIntentHelper.enableReliabilityTriggering(); + + Slog.i(TAG, "Time zone updater / data package tracking enabled"); + } + + /** + * Performs checks that confirm the system image has correctly configured package + * tracking configuration. Only called if package tracking is enabled. Throws an exception if + * the device is configured badly which will prevent the device booting. + */ + private void throwIfDeviceSettingsOrAppsAreBad() { + // None of the checks below can be based on application manifest settings, otherwise a bad + // update could leave the device in an unbootable state. See validateDataAppManifest() and + // validateUpdaterAppManifest() for softer errors. + + throwRuntimeExceptionIfNullOrEmpty( + mUpdateAppPackageName, "Update app package name missing."); + throwRuntimeExceptionIfNullOrEmpty(mDataAppPackageName, "Data app package name missing."); + if (mFailedCheckRetryCount < 1) { + throw logAndThrowRuntimeException("mFailedRetryCount=" + mFailedCheckRetryCount, null); + } + if (mCheckTimeAllowedMillis < 1000) { + throw logAndThrowRuntimeException( + "mCheckTimeAllowedMillis=" + mCheckTimeAllowedMillis, null); + } + + // Validate the updater application package. + // TODO(nfuller) Uncomment or remove the code below. Currently an app stops being a priv-app + // after it is replaced by one in data so this check fails. http://b/35995024 + // try { + // if (!mPackageManagerHelper.isPrivilegedApp(mUpdateAppPackageName)) { + // throw failWithException( + // "Update app " + mUpdateAppPackageName + " must be a priv-app.", null); + // } + // } catch (PackageManager.NameNotFoundException e) { + // throw failWithException("Could not determine update app package details for " + // + mUpdateAppPackageName, e); + // } + // TODO(nfuller) Consider permission checks. While an updated system app retains permissions + // obtained by the system version it's not clear how to check them. + Slog.d(TAG, "Update app " + mUpdateAppPackageName + " is valid."); + + // Validate the data application package. + // TODO(nfuller) Uncomment or remove the code below. Currently an app stops being a priv-app + // after it is replaced by one in data. http://b/35995024 + // try { + // if (!mPackageManagerHelper.isPrivilegedApp(mDataAppPackageName)) { + // throw failWithException( + // "Data app " + mDataAppPackageName + " must be a priv-app.", null); + // } + // } catch (PackageManager.NameNotFoundException e) { + // throw failWithException("Could not determine data app package details for " + // + mDataAppPackageName, e); + // } + // TODO(nfuller) Consider permission checks. While an updated system app retains permissions + // obtained by the system version it's not clear how to check them. + Slog.d(TAG, "Data app " + mDataAppPackageName + " is valid."); + } + + /** + * Inspects the current in-memory state, installed packages and storage state to determine if an + * update check is needed and then trigger if it is. + * + * @param packageChanged true if this method was called because a known packaged definitely + * changed, false if the cause is a reliability trigger + */ + @Override + public synchronized void triggerUpdateIfNeeded(boolean packageChanged) { + if (!mTrackingEnabled) { + throw new IllegalStateException("Unexpected call. Tracking is disabled."); + } + + // Validate the applications' current manifest entries: make sure they are configured as + // they should be. These are not fatal and just means that no update is triggered: we don't + // want to take down the system server if an OEM or Google have pushed a bad update to + // an application. + boolean updaterAppManifestValid = validateUpdaterAppManifest(); + boolean dataAppManifestValid = validateDataAppManifest(); + if (!updaterAppManifestValid || !dataAppManifestValid) { + Slog.e(TAG, "No update triggered due to invalid application manifest entries." + + " updaterApp=" + updaterAppManifestValid + + ", dataApp=" + dataAppManifestValid); + + // There's no point in doing reliability checks if the current packages are bad. + mIntentHelper.disableReliabilityTriggering(); + return; + } + + if (!packageChanged) { + // This call was made because the device is doing a "reliability" check. + // 4 possible cases: + // 1) No check has previously triggered since restart. We want to trigger in this case. + // 2) A check has previously triggered and it is in progress. We want to trigger if + // the response is overdue. + // 3) A check has previously triggered and it failed. We want to trigger, but only if + // we're not in a persistent failure state. + // 4) A check has previously triggered and it succeeded. + // We don't want to trigger, and want to stop future triggers. + + if (!mCheckTriggered) { + // Case 1. + Slog.d(TAG, "triggerUpdateIfNeeded: First reliability trigger."); + } else if (isCheckInProgress()) { + // Case 2. + if (!isCheckResponseOverdue()) { + // A check is in progress but hasn't been given time to succeed. + Slog.d(TAG, + "triggerUpdateIfNeeded: checkComplete call is not yet overdue." + + " Not triggering."); + // Not doing any work, but also not disabling future reliability triggers. + return; + } + } else if (mCheckFailureCount > mFailedCheckRetryCount) { + // Case 3. If the system is in some kind of persistent failure state we don't want + // to keep checking, so just stop. + Slog.i(TAG, "triggerUpdateIfNeeded: number of allowed consecutive check failures" + + " exceeded. Stopping reliability triggers until next reboot or package" + + " update."); + mIntentHelper.disableReliabilityTriggering(); + return; + } else if (mCheckFailureCount == 0) { + // Case 4. + Slog.i(TAG, "triggerUpdateIfNeeded: No reliability check required. Last check was" + + " successful."); + mIntentHelper.disableReliabilityTriggering(); + return; + } + } + + // Read the currently installed data / updater package versions. + PackageVersions currentInstalledVersions = lookupInstalledPackageVersions(); + if (currentInstalledVersions == null) { + // This should not happen if the device is configured in a valid way. + Slog.e(TAG, "triggerUpdateIfNeeded: currentInstalledVersions was null"); + mIntentHelper.disableReliabilityTriggering(); + return; + } + + // Establish the current state using package manager and stored state. Determine if we have + // already successfully checked the installed versions. + PackageStatus packageStatus = mPackageStatusStorage.getPackageStatus(); + if (packageStatus == null) { + // This can imply corrupt, uninitialized storage state (e.g. first check ever on a + // device) or after some kind of reset. + Slog.i(TAG, "triggerUpdateIfNeeded: No package status data found. Data check needed."); + } else if (!packageStatus.mVersions.equals(currentInstalledVersions)) { + // The stored package version information differs from the installed version. + // Trigger the check in all cases. + Slog.i(TAG, "triggerUpdateIfNeeded: Stored package versions=" + + packageStatus.mVersions + ", do not match current package versions=" + + currentInstalledVersions + ". Triggering check."); + } else { + Slog.i(TAG, "triggerUpdateIfNeeded: Stored package versions match currently" + + " installed versions, currentInstalledVersions=" + currentInstalledVersions + + ", packageStatus.mCheckStatus=" + packageStatus.mCheckStatus); + if (packageStatus.mCheckStatus == PackageStatus.CHECK_COMPLETED_SUCCESS) { + // The last check succeeded and nothing has changed. Do nothing and disable + // reliability checks. + Slog.i(TAG, "triggerUpdateIfNeeded: Prior check succeeded. No need to trigger."); + mIntentHelper.disableReliabilityTriggering(); + return; + } + } + + // Generate a token to send to the updater app. + CheckToken checkToken = + mPackageStatusStorage.generateCheckToken(currentInstalledVersions); + if (checkToken == null) { + Slog.w(TAG, "triggerUpdateIfNeeded: Unable to generate check token." + + " Not sending check request."); + return; + } + + // Trigger the update check. + mIntentHelper.sendTriggerUpdateCheck(checkToken); + mCheckTriggered = true; + + // Update the reliability check state in case the update fails. + setCheckInProgress(); + + // Enable reliability triggering in case the check doesn't succeed and there is no + // response at all. Enabling reliability triggering is idempotent. + mIntentHelper.enableReliabilityTriggering(); + } + + /** + * Used to record the result of a check. Can be called even if active package tracking is + * disabled. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected synchronized void recordCheckResult(CheckToken checkToken, boolean success) { + Slog.i(TAG, "recordOperationResult: checkToken=" + checkToken + " success=" + success); + + // If package tracking is disabled it means no record-keeping is required. However, we do + // want to clear out any stored state to make it clear that the current state is unknown and + // should tracking become enabled again (perhaps through an OTA) we'd need to perform an + // update check. + if (!mTrackingEnabled) { + // This means an updater has spontaneously modified time zone data without having been + // triggered. This can happen if the OEM is handling their own updates, but we don't + // need to do any tracking in this case. + + if (checkToken == null) { + // This is the expected case if tracking is disabled but an OEM is handling time + // zone installs using their own mechanism. + Slog.d(TAG, "recordCheckResult: Tracking is disabled and no token has been" + + " provided. Resetting tracking state."); + } else { + // This is unexpected. If tracking is disabled then no check token should have been + // generated by the package tracker. An updater should never create its own token. + // This could be a bug in the updater. + Slog.w(TAG, "recordCheckResult: Tracking is disabled and a token " + checkToken + + " has been unexpectedly provided. Resetting tracking state."); + } + mPackageStatusStorage.resetCheckState(); + return; + } + + if (checkToken == null) { + /* + * If the checkToken is null it suggests an install / uninstall / acknowledgement has + * occurred without a prior trigger (or the client didn't return the token it was given + * for some reason, perhaps a bug). + * + * This shouldn't happen under normal circumstances: + * + * If package tracking is enabled, we assume it is the package tracker responsible for + * triggering updates and a token should have been produced and returned. + * + * If the OEM is handling time zone updates case package tracking should be disabled. + * + * This could happen in tests. The device should recover back to a known state by + * itself rather than be left in an invalid state. + * + * We treat this as putting the device into an unknown state and make sure that + * reliability triggering is enabled so we should recover. + */ + Slog.i(TAG, "recordCheckResult: Unexpectedly missing checkToken, resetting" + + " storage state."); + mPackageStatusStorage.resetCheckState(); + + // Enable reliability triggering and reset the failure count so we know that the + // next reliability trigger will do something. + mIntentHelper.enableReliabilityTriggering(); + mCheckFailureCount = 0; + } else { + // This is the expected case when tracking is enabled: a check was triggered and it has + // completed. + boolean recordedCheckCompleteSuccessfully = + mPackageStatusStorage.markChecked(checkToken, success); + if (recordedCheckCompleteSuccessfully) { + // If we have recorded the result (whatever it was) we know there is no check in + // progress. + setCheckComplete(); + + if (success) { + // Since the check was successful, no more reliability checks are required until + // there is a package change. + mIntentHelper.disableReliabilityTriggering(); + mCheckFailureCount = 0; + } else { + // Enable reliability triggering to potentially check again in future. + mIntentHelper.enableReliabilityTriggering(); + mCheckFailureCount++; + } + } else { + // The failure to record the check means an optimistic lock failure and suggests + // that another check was triggered after the token was generated. + Slog.i(TAG, "recordCheckResult: could not update token=" + checkToken + + " with success=" + success + ". Optimistic lock failure"); + + // Enable reliability triggering to potentially try again in future. + mIntentHelper.enableReliabilityTriggering(); + mCheckFailureCount++; + } + } + } + + /** Access to consecutive failure counts for use in tests. */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected int getCheckFailureCountForTests() { + return mCheckFailureCount; + } + + private void setCheckInProgress() { + mLastTriggerTimestamp = mClockHelper.currentTimestamp(); + } + + private void setCheckComplete() { + mLastTriggerTimestamp = null; + } + + private boolean isCheckInProgress() { + return mLastTriggerTimestamp != null; + } + + private boolean isCheckResponseOverdue() { + if (mLastTriggerTimestamp == null) { + return false; + } + // Risk of overflow, but highly unlikely given the implementation and not problematic. + return mClockHelper.currentTimestamp() > mLastTriggerTimestamp + mCheckTimeAllowedMillis; + } + + private PackageVersions lookupInstalledPackageVersions() { + int updatePackageVersion; + int dataPackageVersion; + try { + updatePackageVersion = + mPackageManagerHelper.getInstalledPackageVersion(mUpdateAppPackageName); + dataPackageVersion = + mPackageManagerHelper.getInstalledPackageVersion(mDataAppPackageName); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "lookupInstalledPackageVersions: Unable to resolve installed package" + + " versions", e); + return null; + } + return new PackageVersions(updatePackageVersion, dataPackageVersion); + } + + private boolean validateDataAppManifest() { + // We only want to talk to a provider that exposed by the known data app package + // so we look up the providers exposed by that app and check the well-known authority is + // there. This prevents the case where *even if* the data app doesn't expose the provider + // required, another app cannot expose one to replace it. + if (!mPackageManagerHelper.contentProviderRegistered( + TimeZoneRulesDataContract.AUTHORITY, mDataAppPackageName)) { + // Error! Found the package but it didn't expose the correct provider. + Slog.w(TAG, "validateDataAppManifest: Data app " + mDataAppPackageName + + " does not expose the required provider with authority=" + + TimeZoneRulesDataContract.AUTHORITY); + return false; + } + // TODO(nfuller) Add any permissions checks needed. + return true; + } + + private boolean validateUpdaterAppManifest() { + try { + // The updater app is expected to have the UPDATE_TIME_ZONE_RULES permission. + // The updater app is expected to have a receiver for the intent we are going to trigger + // and require the TRIGGER_TIME_ZONE_RULES_CHECK. + if (!mPackageManagerHelper.usesPermission( + mUpdateAppPackageName, + RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION)) { + Slog.w(TAG, "validateUpdaterAppManifest: Updater app " + mDataAppPackageName + + " does not use permission=" + + RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION); + return false; + } + if (!mPackageManagerHelper.receiverRegistered( + RulesUpdaterContract.createUpdaterIntent(mUpdateAppPackageName), + RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION)) { + return false; + } + + return true; + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "validateUpdaterAppManifest: Updater app " + mDataAppPackageName + + " does not expose the required broadcast receiver.", e); + return false; + } + } + + private static void throwRuntimeExceptionIfNullOrEmpty(String value, String message) { + if (value == null || value.trim().isEmpty()) { + throw logAndThrowRuntimeException(message, null); + } + } + + private static RuntimeException logAndThrowRuntimeException(String message, Throwable cause) { + Slog.wtf(TAG, message, cause); + throw new RuntimeException(message, cause); + } +} diff --git a/services/core/java/com/android/server/timezone/PackageTrackerHelperImpl.java b/services/core/java/com/android/server/timezone/PackageTrackerHelperImpl.java new file mode 100644 index 000000000000..2e0c21bf007d --- /dev/null +++ b/services/core/java/com/android/server/timezone/PackageTrackerHelperImpl.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import com.android.internal.R; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.os.SystemClock; +import android.util.Slog; + +import java.util.List; + +/** + * A single class that implements multiple helper interfaces for use by {@link PackageTracker}. + */ +final class PackageTrackerHelperImpl implements ClockHelper, ConfigHelper, PackageManagerHelper { + + private static final String TAG = "PackageTrackerHelperImpl"; + + private final Context mContext; + private final PackageManager mPackageManager; + + PackageTrackerHelperImpl(Context context) { + mContext = context; + mPackageManager = context.getPackageManager(); + } + + @Override + public boolean isTrackingEnabled() { + return mContext.getResources().getBoolean(R.bool.config_timeZoneRulesUpdateTrackingEnabled); + } + + @Override + public String getUpdateAppPackageName() { + return mContext.getResources().getString(R.string.config_timeZoneRulesUpdaterPackage); + } + + @Override + public String getDataAppPackageName() { + Resources resources = mContext.getResources(); + return resources.getString(R.string.config_timeZoneRulesDataPackage); + } + + @Override + public int getCheckTimeAllowedMillis() { + return mContext.getResources().getInteger( + R.integer.config_timeZoneRulesCheckTimeMillisAllowed); + } + + @Override + public int getFailedCheckRetryCount() { + return mContext.getResources().getInteger(R.integer.config_timeZoneRulesCheckRetryCount); + } + + @Override + public long currentTimestamp() { + // Use of elapsedRealtime() because this is in-memory state and elapsedRealtime() shouldn't + // change if the system clock changes. + return SystemClock.elapsedRealtime(); + } + + @Override + public int getInstalledPackageVersion(String packageName) + throws PackageManager.NameNotFoundException { + int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; + PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags); + return packageInfo.versionCode; + } + + @Override + public boolean isPrivilegedApp(String packageName) throws PackageManager.NameNotFoundException { + int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; + PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags); + return packageInfo.applicationInfo.isPrivilegedApp(); + } + + @Override + public boolean usesPermission(String packageName, String requiredPermissionName) + throws PackageManager.NameNotFoundException { + int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS + | PackageManager.GET_PERMISSIONS; + PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags); + if (packageInfo.requestedPermissions == null) { + return false; + } + for (String requestedPermission : packageInfo.requestedPermissions) { + if (requiredPermissionName.equals(requestedPermission)) { + return true; + } + } + return false; + } + + @Override + public boolean contentProviderRegistered(String authority, String requiredPackageName) { + int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; + ProviderInfo providerInfo = + mPackageManager.resolveContentProvider(authority, flags); + if (providerInfo == null) { + Slog.i(TAG, "contentProviderRegistered: No content provider registered with authority=" + + authority); + return false; + } + boolean packageMatches = + requiredPackageName.equals(providerInfo.applicationInfo.packageName); + if (!packageMatches) { + Slog.i(TAG, "contentProviderRegistered: App with packageName=" + requiredPackageName + + " does not expose the a content provider with authority=" + authority); + return false; + } + return true; + } + + @Override + public boolean receiverRegistered(Intent intent, String requiredPermissionName) + throws PackageManager.NameNotFoundException { + + int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; + List<ResolveInfo> resolveInfo = mPackageManager.queryBroadcastReceivers(intent, flags); + if (resolveInfo.size() != 1) { + Slog.i(TAG, "receiverRegistered: Zero or multiple broadcast receiver registered for" + + " intent=" + intent + ", found=" + resolveInfo); + return false; + } + + ResolveInfo matched = resolveInfo.get(0); + boolean requiresPermission = requiredPermissionName.equals(matched.activityInfo.permission); + if (!requiresPermission) { + Slog.i(TAG, "receiverRegistered: Broadcast receiver registered for intent=" + + intent + " must require permission " + requiredPermissionName); + } + return requiresPermission; + } +} diff --git a/services/core/java/com/android/server/timezone/PackageVersions.java b/services/core/java/com/android/server/timezone/PackageVersions.java new file mode 100644 index 000000000000..fc0d6e1ad51e --- /dev/null +++ b/services/core/java/com/android/server/timezone/PackageVersions.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +/** + * Package version information about the time zone updater and time zone data application packages. + */ +final class PackageVersions { + + final int mUpdateAppVersion; + final int mDataAppVersion; + + PackageVersions(int updateAppVersion, int dataAppVersion) { + this.mUpdateAppVersion = updateAppVersion; + this.mDataAppVersion = dataAppVersion; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PackageVersions that = (PackageVersions) o; + + if (mUpdateAppVersion != that.mUpdateAppVersion) { + return false; + } + return mDataAppVersion == that.mDataAppVersion; + } + + @Override + public int hashCode() { + int result = mUpdateAppVersion; + result = 31 * result + mDataAppVersion; + return result; + } + + @Override + public String toString() { + return "PackageVersions{" + + "mUpdateAppVersion=" + mUpdateAppVersion + + ", mDataAppVersion=" + mDataAppVersion + + '}'; + } +} diff --git a/services/core/java/com/android/server/timezone/PermissionHelper.java b/services/core/java/com/android/server/timezone/PermissionHelper.java new file mode 100644 index 000000000000..ba91c7f7b7ab --- /dev/null +++ b/services/core/java/com/android/server/timezone/PermissionHelper.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +/** + * An easy-to-mock interface around permission checks for use by {@link RulesManagerService}. + */ +public interface PermissionHelper { + + void enforceCallerHasPermission(String requiredPermission) throws SecurityException; +} diff --git a/services/core/java/com/android/server/timezone/RulesManagerService.java b/services/core/java/com/android/server/timezone/RulesManagerService.java new file mode 100644 index 000000000000..82bd35679b04 --- /dev/null +++ b/services/core/java/com/android/server/timezone/RulesManagerService.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemService; + +import android.app.timezone.Callback; +import android.app.timezone.DistroFormatVersion; +import android.app.timezone.DistroRulesVersion; +import android.app.timezone.ICallback; +import android.app.timezone.IRulesManager; +import android.app.timezone.RulesManager; +import android.app.timezone.RulesState; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Slog; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import libcore.tzdata.shared2.DistroException; +import libcore.tzdata.shared2.DistroVersion; +import libcore.tzdata.shared2.StagedDistroOperation; +import libcore.tzdata.update2.TimeZoneDistroInstaller; + +// TODO(nfuller) Add EventLog calls where useful in the system server. +// TODO(nfuller) Check logging best practices in the system server. +// TODO(nfuller) Check error handling best practices in the system server. +public final class RulesManagerService extends IRulesManager.Stub { + + private static final String TAG = "timezone.RulesManagerService"; + + /** The distro format supported by this device. */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + static final DistroFormatVersion DISTRO_FORMAT_VERSION_SUPPORTED = + new DistroFormatVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION); + + public static class Lifecycle extends SystemService { + private RulesManagerService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + mService = RulesManagerService.create(getContext()); + mService.start(); + + publishBinderService(Context.TIME_ZONE_RULES_MANAGER_SERVICE, mService); + } + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + static final String REQUIRED_UPDATER_PERMISSION = + android.Manifest.permission.UPDATE_TIME_ZONE_RULES; + private static final File SYSTEM_TZ_DATA_FILE = new File("/system/usr/share/zoneinfo/tzdata"); + private static final File TZ_DATA_DIR = new File("/data/misc/zoneinfo"); + + private final AtomicBoolean mOperationInProgress = new AtomicBoolean(false); + private final PermissionHelper mPermissionHelper; + private final PackageTracker mPackageTracker; + private final Executor mExecutor; + private final TimeZoneDistroInstaller mInstaller; + private final FileDescriptorHelper mFileDescriptorHelper; + + private static RulesManagerService create(Context context) { + RulesManagerServiceHelperImpl helper = new RulesManagerServiceHelperImpl(context); + return new RulesManagerService( + helper /* permissionHelper */, + helper /* executor */, + helper /* fileDescriptorHelper */, + PackageTracker.create(context), + new TimeZoneDistroInstaller(TAG, SYSTEM_TZ_DATA_FILE, TZ_DATA_DIR)); + } + + // A constructor that can be used by tests to supply mocked / faked dependencies. + RulesManagerService(PermissionHelper permissionHelper, + Executor executor, + FileDescriptorHelper fileDescriptorHelper, PackageTracker packageTracker, + TimeZoneDistroInstaller timeZoneDistroInstaller) { + mPermissionHelper = permissionHelper; + mExecutor = executor; + mFileDescriptorHelper = fileDescriptorHelper; + mPackageTracker = packageTracker; + mInstaller = timeZoneDistroInstaller; + } + + public void start() { + mPackageTracker.start(); + } + + @Override // Binder call + public RulesState getRulesState() { + mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); + + synchronized(this) { + String systemRulesVersion; + try { + systemRulesVersion = mInstaller.getSystemRulesVersion(); + } catch (IOException e) { + Slog.w(TAG, "Failed to read system rules", e); + return null; + } + + boolean operationInProgress = this.mOperationInProgress.get(); + + // Determine the staged operation status, if possible. + DistroRulesVersion stagedDistroRulesVersion = null; + int stagedOperationStatus = RulesState.STAGED_OPERATION_UNKNOWN; + if (!operationInProgress) { + StagedDistroOperation stagedDistroOperation; + try { + stagedDistroOperation = mInstaller.getStagedDistroOperation(); + if (stagedDistroOperation == null) { + stagedOperationStatus = RulesState.STAGED_OPERATION_NONE; + } else if (stagedDistroOperation.isUninstall) { + stagedOperationStatus = RulesState.STAGED_OPERATION_UNINSTALL; + } else { + // Must be an install. + stagedOperationStatus = RulesState.STAGED_OPERATION_INSTALL; + DistroVersion stagedDistroVersion = stagedDistroOperation.distroVersion; + stagedDistroRulesVersion = new DistroRulesVersion( + stagedDistroVersion.rulesVersion, + stagedDistroVersion.revision); + } + } catch (DistroException | IOException e) { + Slog.w(TAG, "Failed to read staged distro.", e); + } + } + + // Determine the installed distro state, if possible. + DistroVersion installedDistroVersion; + int distroStatus = RulesState.DISTRO_STATUS_UNKNOWN; + DistroRulesVersion installedDistroRulesVersion = null; + if (!operationInProgress) { + try { + installedDistroVersion = mInstaller.getInstalledDistroVersion(); + if (installedDistroVersion == null) { + distroStatus = RulesState.DISTRO_STATUS_NONE; + installedDistroRulesVersion = null; + } else { + distroStatus = RulesState.DISTRO_STATUS_INSTALLED; + installedDistroRulesVersion = new DistroRulesVersion( + installedDistroVersion.rulesVersion, + installedDistroVersion.revision); + } + } catch (DistroException | IOException e) { + Slog.w(TAG, "Failed to read installed distro.", e); + } + } + return new RulesState(systemRulesVersion, DISTRO_FORMAT_VERSION_SUPPORTED, + operationInProgress, stagedOperationStatus, stagedDistroRulesVersion, + distroStatus, installedDistroRulesVersion); + } + } + + @Override + public int requestInstall( + ParcelFileDescriptor timeZoneDistro, byte[] checkTokenBytes, ICallback callback) { + mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); + + CheckToken checkToken = null; + if (checkTokenBytes != null) { + checkToken = createCheckTokenOrThrow(checkTokenBytes); + } + synchronized (this) { + if (timeZoneDistro == null) { + throw new NullPointerException("timeZoneDistro == null"); + } + if (callback == null) { + throw new NullPointerException("observer == null"); + } + if (mOperationInProgress.get()) { + return RulesManager.ERROR_OPERATION_IN_PROGRESS; + } + mOperationInProgress.set(true); + + // Execute the install asynchronously. + mExecutor.execute(new InstallRunnable(timeZoneDistro, checkToken, callback)); + + return RulesManager.SUCCESS; + } + } + + private class InstallRunnable implements Runnable { + + private final ParcelFileDescriptor mTimeZoneDistro; + private final CheckToken mCheckToken; + private final ICallback mCallback; + + InstallRunnable( + ParcelFileDescriptor timeZoneDistro, CheckToken checkToken, ICallback callback) { + mTimeZoneDistro = timeZoneDistro; + mCheckToken = checkToken; + mCallback = callback; + } + + @Override + public void run() { + // Adopt the ParcelFileDescriptor into this try-with-resources so it is closed + // when we are done. + boolean success = false; + try { + byte[] distroBytes = + RulesManagerService.this.mFileDescriptorHelper.readFully(mTimeZoneDistro); + int installerResult = mInstaller.stageInstallWithErrorCode(distroBytes); + int resultCode = mapInstallerResultToApiCode(installerResult); + sendFinishedStatus(mCallback, resultCode); + + // All the installer failure modes are currently non-recoverable and won't be + // improved by trying again. Therefore success = true. + success = true; + } catch (Exception e) { + Slog.w(TAG, "Failed to install distro.", e); + sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE); + } finally { + // Notify the package tracker that the operation is now complete. + mPackageTracker.recordCheckResult(mCheckToken, success); + + mOperationInProgress.set(false); + } + } + + private int mapInstallerResultToApiCode(int installerResult) { + switch (installerResult) { + case TimeZoneDistroInstaller.INSTALL_SUCCESS: + return Callback.SUCCESS; + case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE: + return Callback.ERROR_INSTALL_BAD_DISTRO_STRUCTURE; + case TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD: + return Callback.ERROR_INSTALL_RULES_TOO_OLD; + case TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION: + return Callback.ERROR_INSTALL_BAD_DISTRO_FORMAT_VERSION; + case TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR: + return Callback.ERROR_INSTALL_VALIDATION_ERROR; + default: + return Callback.ERROR_UNKNOWN_FAILURE; + } + } + } + + @Override + public int requestUninstall(byte[] checkTokenBytes, ICallback callback) { + mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); + + CheckToken checkToken = null; + if (checkTokenBytes != null) { + checkToken = createCheckTokenOrThrow(checkTokenBytes); + } + synchronized(this) { + if (callback == null) { + throw new NullPointerException("callback == null"); + } + + if (mOperationInProgress.get()) { + return RulesManager.ERROR_OPERATION_IN_PROGRESS; + } + mOperationInProgress.set(true); + + // Execute the uninstall asynchronously. + mExecutor.execute(new UninstallRunnable(checkToken, callback)); + + return RulesManager.SUCCESS; + } + } + + private class UninstallRunnable implements Runnable { + + private final CheckToken mCheckToken; + private final ICallback mCallback; + + public UninstallRunnable(CheckToken checkToken, ICallback callback) { + mCheckToken = checkToken; + mCallback = callback; + } + + @Override + public void run() { + boolean success = false; + try { + success = mInstaller.stageUninstall(); + // Right now we just have success (0) / failure (1). All clients should be checking + // against SUCCESS. More granular failures may be added in future. + int resultCode = success ? Callback.SUCCESS + : Callback.ERROR_UNKNOWN_FAILURE; + sendFinishedStatus(mCallback, resultCode); + } catch (Exception e) { + Slog.w(TAG, "Failed to uninstall distro.", e); + sendFinishedStatus(mCallback, Callback.ERROR_UNKNOWN_FAILURE); + } finally { + // Notify the package tracker that the operation is now complete. + mPackageTracker.recordCheckResult(mCheckToken, success); + + mOperationInProgress.set(false); + } + } + } + + private void sendFinishedStatus(ICallback callback, int resultCode) { + try { + callback.onFinished(resultCode); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify observer of result", e); + } + } + + @Override + public void requestNothing(byte[] checkTokenBytes, boolean success) { + mPermissionHelper.enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); + CheckToken checkToken = null; + if (checkTokenBytes != null) { + checkToken = createCheckTokenOrThrow(checkTokenBytes); + } + mPackageTracker.recordCheckResult(checkToken, success); + } + + private static CheckToken createCheckTokenOrThrow(byte[] checkTokenBytes) { + CheckToken checkToken; + try { + checkToken = CheckToken.fromByteArray(checkTokenBytes); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to read token bytes " + + Arrays.toString(checkTokenBytes), e); + } + return checkToken; + } +} diff --git a/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java new file mode 100644 index 000000000000..15a571d6750c --- /dev/null +++ b/services/core/java/com/android/server/timezone/RulesManagerServiceHelperImpl.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import android.content.Context; +import android.os.ParcelFileDescriptor; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.concurrent.Executor; +import libcore.io.Streams; + +/** + * A single class that implements multiple helper interfaces for use by {@link RulesManagerService}. + */ +final class RulesManagerServiceHelperImpl + implements PermissionHelper, Executor, FileDescriptorHelper { + + private final Context mContext; + + RulesManagerServiceHelperImpl(Context context) { + mContext = context; + } + + @Override + public void enforceCallerHasPermission(String requiredPermission) { + mContext.enforceCallingPermission(requiredPermission, null /* message */); + } + + // TODO Wake lock required? + @Override + public void execute(Runnable runnable) { + // TODO Is there a better way? + new Thread(runnable).start(); + } + + @Override + public byte[] readFully(ParcelFileDescriptor parcelFileDescriptor) throws IOException { + try (ParcelFileDescriptor pfd = parcelFileDescriptor) { + // Read bytes + FileInputStream in = new FileInputStream(pfd.getFileDescriptor(), false /* isOwner */); + return Streams.readFully(in); + } + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b4e806bfdd0c..e1cbc91c16b9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -171,6 +171,8 @@ public final class SystemServer { "com.android.server.content.ContentService$Lifecycle"; private static final String WALLPAPER_SERVICE_CLASS = "com.android.server.wallpaper.WallpaperManagerService$Lifecycle"; + private static final String TIME_ZONE_RULES_MANAGER_SERVICE_CLASS = + "com.android.server.timezone.RulesManagerService$Lifecycle"; private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; @@ -978,6 +980,13 @@ public final class SystemServer { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } + if (!disableNonCoreServices && context.getResources().getBoolean( + R.bool.config_enableUpdateableTimeZoneRules)) { + traceBeginAndSlog("StartTimeZoneRulesManagerService"); + mSystemServiceManager.startService(TIME_ZONE_RULES_MANAGER_SERVICE_CLASS); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + traceBeginAndSlog("StartAudioService"); mSystemServiceManager.startService(AudioService.Lifecycle.class); Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); diff --git a/services/tests/servicestests/src/com/android/server/timezone/CheckTokenTest.java b/services/tests/servicestests/src/com/android/server/timezone/CheckTokenTest.java new file mode 100644 index 000000000000..9603a06aaa66 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezone/CheckTokenTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import org.junit.Test; + +import android.support.test.filters.SmallTest; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +@SmallTest +public class CheckTokenTest { + + @Test + public void toByteArray() throws Exception { + PackageVersions packageVersions = + new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */); + CheckToken originalToken = new CheckToken(1 /* optimisticLockId */, packageVersions); + assertEquals(originalToken, CheckToken.fromByteArray(originalToken.toByteArray())); + } + + @Test + public void fromByteArray() { + PackageVersions packageVersions = + new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */); + CheckToken token = new CheckToken(1, packageVersions); + byte[] validTokenBytes = token.toByteArray(); + byte[] shortTokenBytes = new byte[validTokenBytes.length - 1]; + System.arraycopy(validTokenBytes, 0, shortTokenBytes, 0, shortTokenBytes.length); + + try { + CheckToken.fromByteArray(shortTokenBytes); + fail(); + } catch (IOException expected) {} + } + + @Test + public void equals() { + PackageVersions packageVersions1 = + new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */); + PackageVersions packageVersions2 = + new PackageVersions(2 /* updateAppVersion */, 2 /* dataAppVersion */); + assertFalse(packageVersions1.equals(packageVersions2)); + + CheckToken baseline = new CheckToken(1, packageVersions1); + assertEquals(baseline, baseline); + + CheckToken deepEqual = new CheckToken(1, packageVersions1); + assertEquals(baseline, deepEqual); + + CheckToken differentOptimisticLockId = new CheckToken(2, packageVersions1); + assertFalse(differentOptimisticLockId.equals(baseline)); + + CheckToken differentPackageVersions = new CheckToken(1, packageVersions2); + assertFalse(differentPackageVersions.equals(baseline)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java new file mode 100644 index 000000000000..e085270fc3a4 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; + +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@SmallTest +public class PackageStatusStorageTest { + private static final PackageVersions VALID_PACKAGE_VERSIONS = new PackageVersions(1, 2); + + private PackageStatusStorage mPackageStatusStorage; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + + // Using the instrumentation context means the database is created in a test app-specific + // directory. + mPackageStatusStorage = new PackageStatusStorage(context); + } + + @After + public void tearDown() throws Exception { + mPackageStatusStorage.deleteDatabaseForTests(); + } + + @Test + public void getPackageStatus_initialState() { + assertNull(mPackageStatusStorage.getPackageStatus()); + } + + @Test + public void resetCheckState() { + // Assert initial state. + assertNull(mPackageStatusStorage.getPackageStatus()); + + CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + + // There should now be a state. + assertNotNull(mPackageStatusStorage.getPackageStatus()); + + // Now clear the state. + mPackageStatusStorage.resetCheckState(); + + // After reset, there should be no package state again. + assertNull(mPackageStatusStorage.getPackageStatus()); + + CheckToken token2 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + + // Token after a reset should still be distinct. + assertFalse(token1.equals(token2)); + + // Now clear the state again. + mPackageStatusStorage.resetCheckState(); + + // After reset, there should be no package state again. + assertNull(mPackageStatusStorage.getPackageStatus()); + + CheckToken token3 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + + // A CheckToken generated after a reset should still be distinct. + assertFalse(token2.equals(token3)); + } + + @Test + public void generateCheckToken_missingRowBehavior() { + // Assert initial state. + assertNull(mPackageStatusStorage.getPackageStatus()); + + CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + assertNotNull(token1); + + // There should now be state. + assertNotNull(mPackageStatusStorage.getPackageStatus()); + + // Corrupt the table by removing the one row. + mPackageStatusStorage.deleteRowForTests(); + + // Check that generateCheckToken recovers. + assertNotNull(mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS)); + } + + @Test + public void getPackageStatus_missingRowBehavior() { + // Assert initial state. + assertNull(mPackageStatusStorage.getPackageStatus()); + + CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + assertNotNull(token1); + + // There should now be a state. + assertNotNull(mPackageStatusStorage.getPackageStatus()); + + // Corrupt the table by removing the one row. + mPackageStatusStorage.deleteRowForTests(); + + assertNull(mPackageStatusStorage.getPackageStatus()); + } + + @Test + public void markChecked_missingRowBehavior() { + // Assert initial state. + CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + assertNotNull(token1); + + // There should now be a state. + assertNotNull(mPackageStatusStorage.getPackageStatus()); + + // Corrupt the table by removing the one row. + mPackageStatusStorage.deleteRowForTests(); + + // The missing row should mean token1 is now considered invalid, so we should get a false. + assertFalse(mPackageStatusStorage.markChecked(token1, true /* succeeded */)); + + // The storage should have recovered and we should be able to carry on like before. + CheckToken token2 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); + assertTrue(mPackageStatusStorage.markChecked(token2, true /* succeeded */)); + } + + @Test + public void checkToken_tokenIsUnique() { + PackageVersions packageVersions = VALID_PACKAGE_VERSIONS; + PackageStatus expectedPackageStatus = + new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions); + + CheckToken token1 = mPackageStatusStorage.generateCheckToken(packageVersions); + assertEquals(packageVersions, token1.mPackageVersions); + + PackageStatus actualPackageStatus1 = mPackageStatusStorage.getPackageStatus(); + assertEquals(expectedPackageStatus, actualPackageStatus1); + + CheckToken token2 = mPackageStatusStorage.generateCheckToken(packageVersions); + assertEquals(packageVersions, token1.mPackageVersions); + assertFalse(token1.mOptimisticLockId == token2.mOptimisticLockId); + assertFalse(token1.equals(token2)); + } + + @Test + public void markChecked_checkSucceeded() { + PackageVersions packageVersions = VALID_PACKAGE_VERSIONS; + + CheckToken token = mPackageStatusStorage.generateCheckToken(packageVersions); + boolean writeOk = mPackageStatusStorage.markChecked(token, true /* succeeded */); + assertTrue(writeOk); + + PackageStatus expectedPackageStatus = + new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions); + assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus()); + } + + @Test + public void markChecked_checkFailed() { + PackageVersions packageVersions = VALID_PACKAGE_VERSIONS; + + CheckToken token = mPackageStatusStorage.generateCheckToken(packageVersions); + boolean writeOk = mPackageStatusStorage.markChecked(token, false /* succeeded */); + assertTrue(writeOk); + + PackageStatus expectedPackageStatus = + new PackageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, packageVersions); + assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus()); + } + + @Test + public void markChecked_optimisticLocking_multipleToken() { + PackageVersions packageVersions = VALID_PACKAGE_VERSIONS; + CheckToken token1 = mPackageStatusStorage.generateCheckToken(packageVersions); + CheckToken token2 = mPackageStatusStorage.generateCheckToken(packageVersions); + + PackageStatus packageStatusBeforeChecked = mPackageStatusStorage.getPackageStatus(); + + boolean writeOk1 = mPackageStatusStorage.markChecked(token1, true /* succeeded */); + // Generation of token2 should mean that token1 is no longer valid. + assertFalse(writeOk1); + assertEquals(packageStatusBeforeChecked, mPackageStatusStorage.getPackageStatus()); + + boolean writeOk2 = mPackageStatusStorage.markChecked(token2, true /* succeeded */); + // token2 should still be valid, and the attempt with token1 should have had no effect. + assertTrue(writeOk2); + PackageStatus expectedPackageStatus = + new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions); + assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus()); + } + + @Test + public void markChecked_optimisticLocking_repeatedTokenUse() { + PackageVersions packageVersions = VALID_PACKAGE_VERSIONS; + CheckToken token = mPackageStatusStorage.generateCheckToken(packageVersions); + + boolean writeOk1 = mPackageStatusStorage.markChecked(token, true /* succeeded */); + assertTrue(writeOk1); + + PackageStatus expectedPackageStatus = + new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions); + assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus()); + + // token cannot be reused. + boolean writeOk2 = mPackageStatusStorage.markChecked(token, true /* succeeded */); + assertFalse(writeOk2); + assertEquals(expectedPackageStatus, mPackageStatusStorage.getPackageStatus()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusTest.java new file mode 100644 index 000000000000..c0ae81e30049 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import org.junit.Test; + +import android.support.test.filters.SmallTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SmallTest +public class PackageStatusTest { + + @Test + public void equals() { + PackageVersions packageVersions1 = + new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */); + PackageVersions packageVersions2 = + new PackageVersions(2 /* updateAppVersion */, 1 /* dataAppVersion */); + assertFalse(packageVersions1.equals(packageVersions2)); + + PackageStatus baseline = + new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions1); + assertEquals(baseline, baseline); + + PackageStatus deepEqual = + new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions1); + assertEquals(baseline, deepEqual); + + PackageStatus differentStatus = + new PackageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions1); + assertFalse(differentStatus.equals(baseline)); + + PackageStatus differentPackageVersions = + new PackageStatus(PackageStatus.CHECK_STARTED, packageVersions2); + assertFalse(differentPackageVersions.equals(baseline)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java new file mode 100644 index 000000000000..45b0af37f9e3 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java @@ -0,0 +1,1471 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import android.app.timezone.RulesUpdaterContract; +import android.content.Context; +import android.content.Intent; +import android.provider.TimeZoneRulesDataContract; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.hamcrest.MockitoHamcrest.argThat; + +/** + * White box interaction / unit testing of the {@link PackageTracker}. + */ +@SmallTest +public class PackageTrackerTest { + private static final String UPDATE_APP_PACKAGE_NAME = "updateAppPackageName"; + private static final String DATA_APP_PACKAGE_NAME = "dataAppPackageName"; + private static final PackageVersions INITIAL_APP_PACKAGE_VERSIONS = + new PackageVersions(2 /* updateAppVersion */, 2 /* dataAppVersion */); + + private ConfigHelper mMockConfigHelper; + private PackageManagerHelper mMockPackageManagerHelper; + + private FakeClockHelper mFakeClock; + private FakeIntentHelper mFakeIntentHelper; + private PackageStatusStorage mPackageStatusStorage; + private PackageTracker mPackageTracker; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + + mFakeClock = new FakeClockHelper(); + + // Read-only interfaces so are easy to mock. + mMockConfigHelper = mock(ConfigHelper.class); + mMockPackageManagerHelper = mock(PackageManagerHelper.class); + + // Using the instrumentation context means the database is created in a test app-specific + // directory. We can use the real thing for this test. + mPackageStatusStorage = new PackageStatusStorage(context); + + // For other interactions with the Android framework we create a fake object. + mFakeIntentHelper = new FakeIntentHelper(); + + // Create the PackageTracker to use in tests. + mPackageTracker = new PackageTracker( + mFakeClock, + mMockConfigHelper, + mMockPackageManagerHelper, + mPackageStatusStorage, + mFakeIntentHelper); + } + + @After + public void tearDown() throws Exception { + if (mPackageStatusStorage != null) { + mPackageStatusStorage.deleteDatabaseForTests(); + } + } + + @Test + public void trackingDisabled_intentHelperNotUsed() { + // Set up device configuration. + configureTrackingDisabled(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the IntentHelper was not initialized. + mFakeIntentHelper.assertNotInitialized(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + } + + @Test + public void trackingDisabled_triggerUpdateIfNeededNotAllowed() { + // Set up device configuration. + configureTrackingDisabled(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + try { + // This call should also not be allowed and will throw an exception if tracking is + // disabled. + mPackageTracker.triggerUpdateIfNeeded(true); + fail(); + } catch (IllegalStateException expected) {} + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + } + + @Test + public void trackingDisabled_unsolicitedResultsIgnored_withoutToken() { + // Set up device configuration. + configureTrackingDisabled(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Receiving a check result when tracking is disabled should cause the storage to be + // reset. + mPackageTracker.recordCheckResult(null /* checkToken */, true /* success */); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Assert the storage was reset. + checkPackageStorageStatusIsInitialOrReset(); + } + + @Test + public void trackingDisabled_unsolicitedResultsIgnored_withToken() { + // Set up device configuration. + configureTrackingDisabled(); + + // Set the storage into an arbitrary state so we can detect a reset. + mPackageStatusStorage.generateCheckToken(INITIAL_APP_PACKAGE_VERSIONS); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Receiving a check result when tracking is disabled should cause the storage to be reset. + mPackageTracker.recordCheckResult(createArbitraryCheckToken(), true /* success */); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Assert the storage was reset. + checkPackageStorageStatusIsInitialOrReset(); + } + + @Test + public void trackingEnabled_updateAppConfigMissing() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureUpdateAppPackageNameMissing(); + configureDataAppPackageOk(DATA_APP_PACKAGE_NAME); + + try { + // Initialize the tracker. + mPackageTracker.start(); + fail(); + } catch (RuntimeException expected) {} + + mFakeIntentHelper.assertNotInitialized(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + } + + // TODO(nfuller): Uncomment or delete when it's clear what will happen with http://b/35995024 + // @Test + // public void trackingEnabled_updateAppNotPrivileged() throws Exception { + // // Set up device configuration. + // configureTrackingEnabled(); + // configureReliabilityConfigSettingsOk(); + // configureUpdateAppPackageNotPrivileged(UPDATE_APP_PACKAGE_NAME); + // configureDataAppPackageOk(DATA_APP_PACKAGE_NAME); + // + // try { + // // Initialize the tracker. + // mPackageTracker.start(); + // fail(); + // } catch (RuntimeException expected) {} + // + // mFakeIntentHelper.assertNotInitialized(); + // + // // Check reliability triggering state. + // mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + // } + + @Test + public void trackingEnabled_dataAppConfigMissing() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureUpdateAppPackageOk(UPDATE_APP_PACKAGE_NAME); + configureDataAppPackageNameMissing(); + + try { + // Initialize the tracker. + mPackageTracker.start(); + fail(); + } catch (RuntimeException expected) {} + + mFakeIntentHelper.assertNotInitialized(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + } + + // TODO(nfuller): Uncomment or delete when it's clear what will happen with http://b/35995024 + // @Test + // public void trackingEnabled_dataAppNotPrivileged() throws Exception { + // // Set up device configuration. + // configureTrackingEnabled(); + // configureReliabilityConfigSettingsOk(); + // configureUpdateAppPackageOk(UPDATE_APP_PACKAGE_NAME); + // configureDataAppPackageNotPrivileged(DATA_APP_PACKAGE_NAME); + // + // try { + // // Initialize the tracker. + // mPackageTracker.start(); + // fail(); + // } catch (RuntimeException expected) {} + // + // mFakeIntentHelper.assertNotInitialized(); + // + // // Check reliability triggering state. + // mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + // } + + @Test + public void trackingEnabled_packageUpdate_badUpdateAppManifestEntry() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Configure a bad manifest for the update app. Should effectively turn off tracking. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + configureUpdateAppManifestBad(UPDATE_APP_PACKAGE_NAME); + configureDataAppManifestOk(DATA_APP_PACKAGE_NAME); + configureUpdateAppPackageVersion( + UPDATE_APP_PACKAGE_NAME, packageVersions.mUpdateAppVersion); + configureDataAppPackageVersion(DATA_APP_PACKAGE_NAME, packageVersions.mDataAppVersion); + // Simulate a tracked package being updated. + mFakeIntentHelper.simulatePackageUpdatedEvent(); + + // Assert the PackageTracker did not attempt to trigger an update. + mFakeIntentHelper.assertUpdateNotTriggered(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Assert the storage was not touched. + checkPackageStorageStatusIsInitialOrReset(); + } + + @Test + public void trackingEnabled_packageUpdate_badDataAppManifestEntry() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Configure a bad manifest for the data app. Should effectively turn off tracking. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + configureUpdateAppManifestOk(UPDATE_APP_PACKAGE_NAME); + configureDataAppManifestBad(DATA_APP_PACKAGE_NAME); + configureUpdateAppPackageVersion( + UPDATE_APP_PACKAGE_NAME, packageVersions.mUpdateAppVersion); + configureDataAppPackageVersion(DATA_APP_PACKAGE_NAME, packageVersions.mDataAppVersion); + mFakeIntentHelper.simulatePackageUpdatedEvent(); + + // Assert the PackageTracker did not attempt to trigger an update. + mFakeIntentHelper.assertUpdateNotTriggered(); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Assert the storage was not touched. + checkPackageStorageStatusIsInitialOrReset(); + } + + @Test + public void trackingEnabled_packageUpdate_responseWithToken_success() throws Exception { + trackingEnabled_packageUpdate_responseWithToken(true); + } + + @Test + public void trackingEnabled_packageUpdate_responseWithToken_failed() throws Exception { + trackingEnabled_packageUpdate_responseWithToken(false); + } + + private void trackingEnabled_packageUpdate_responseWithToken(boolean success) throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate a tracked package being updated. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + // Get the token that was passed to the intent helper, and pass it back. + CheckToken token = mFakeIntentHelper.captureAndResetLastToken(); + mPackageTracker.recordCheckResult(token, success); + + // Check storage and reliability triggering state. + if (success) { + checkUpdateCheckSuccessful(packageVersions); + } else { + checkUpdateCheckFailed(packageVersions); + } + } + + @Test + public void trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset_success() + throws Exception { + trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset(true); + } + + @Test + public void trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset_failed() + throws Exception { + trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset(false); + } + + private void trackingEnabled_packageUpdate_responseWithoutTokenCausesStorageReset( + boolean success) throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Set up installed app versions / manifests. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + // Ignore the token that was given to the intent helper, just pass null. + mPackageTracker.recordCheckResult(null /* checkToken */, success); + + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Assert the storage was reset. + checkPackageStorageStatusIsInitialOrReset(); + } + + /** + * Two package updates triggered for the same package versions. The second is triggered while + * the first is still happening. + */ + @Test + public void trackingEnabled_packageUpdate_twoChecksNoPackageChange_secondWhileFirstInProgress() + throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate package installation. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + // Get the first token. + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions, token1.mPackageVersions); + + // Now attempt to generate another check while the first is in progress and without having + // updated the package versions. The PackageTracker should trigger again for safety. + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions, token2.mPackageVersions); + assertEquals(token1.mPackageVersions, token2.mPackageVersions); + assertTrue(token1.mOptimisticLockId != token2.mOptimisticLockId); + } + + /** + * Two package updates triggered for the same package versions. The second happens after + * the first has succeeded. + */ + @Test + public void trackingEnabled_packageUpdate_twoChecksNoPackageChange_sequential() + throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate package installation. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + // Get the token. + CheckToken token = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions, token.mPackageVersions); + + // Simulate a successful check. + mPackageTracker.recordCheckResult(token, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions); + + // Now attempt to generate another check, but without having updated the package. The + // PackageTracker should be smart enough to recognize there's nothing to do here. + simulatePackageInstallation(packageVersions); + + // Assert the PackageTracker did not attempt to trigger an update. + mFakeIntentHelper.assertUpdateNotTriggered(); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions); + } + + /** + * Two package updates triggered for the same package versions. The second is triggered after + * the first has failed. + */ + @Test + public void trackingEnabled_packageUpdate_afterFailure() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate package installation. + PackageVersions packageVersions = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + // Get the first token. + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions, token1.mPackageVersions); + + // Simulate an *unsuccessful* check. + mPackageTracker.recordCheckResult(token1, false /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckFailed(packageVersions); + + // Now generate another check, but without having updated the package. The + // PackageTracker should recognize the last check failed and trigger again. + simulatePackageInstallation(packageVersions); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions); + + // Get the second token. + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + + // Assert some things about the tokens. + assertEquals(packageVersions, token2.mPackageVersions); + assertTrue(token1.mOptimisticLockId != token2.mOptimisticLockId); + + // For completeness, now simulate this check was successful. + mPackageTracker.recordCheckResult(token2, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions); + } + + /** + * Two package updates triggered for different package versions. The second is triggered while + * the first is still happening. + */ + @Test + public void trackingEnabled_packageUpdate_twoChecksWithPackageChange_firstCheckInProcess() + throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate package installation. + PackageVersions packageVersions1 = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions1); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions1); + + // Get the first token. + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions1, token1.mPackageVersions); + + // Simulate a tracked package being updated a second time (before the response for the + // first has been received). + PackageVersions packageVersions2 = + new PackageVersions(3 /* updateAppPackageVersion */, 4 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions2); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions2); + + // Get the second token. + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions2, token2.mPackageVersions); + + // token1 should be invalid because the token2 was generated. + mPackageTracker.recordCheckResult(token1, true /* success */); + + // Reliability triggering should still be enabled. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Check the expected storage state. + checkPackageStorageStatus(PackageStatus.CHECK_STARTED, packageVersions2); + + // token2 should still be accepted. + mPackageTracker.recordCheckResult(token2, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions2); + } + + /** + * Two package updates triggered for different package versions. The second is triggered after + * the first has completed successfully. + */ + @Test + public void trackingEnabled_packageUpdate_twoChecksWithPackageChange_sequential() + throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate package installation. + PackageVersions packageVersions1 = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions1); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions1); + + // Get the first token. + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions1, token1.mPackageVersions); + + // token1 should be accepted. + mPackageTracker.recordCheckResult(token1, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions1); + + // Simulate a tracked package being updated a second time. + PackageVersions packageVersions2 = + new PackageVersions(3 /* updateAppPackageVersion */, 4 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions2); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions2); + + // Get the second token. + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions2, token2.mPackageVersions); + + // token2 should still be accepted. + mPackageTracker.recordCheckResult(token2, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions2); + } + + /** + * Replaying the same token twice. + */ + @Test + public void trackingEnabled_packageUpdate_sameTokenReplayFails() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + configureValidApplications(); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate package installation. + PackageVersions packageVersions1 = + new PackageVersions(2 /* updateAppPackageVersion */, 3 /* dataAppPackageVersion */); + simulatePackageInstallation(packageVersions1); + + // Confirm an update was triggered. + checkUpdateCheckTriggered(packageVersions1); + + // Get the first token. + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions1, token1.mPackageVersions); + + // token1 should be accepted. + mPackageTracker.recordCheckResult(token1, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions1); + + // Apply token1 again. + mPackageTracker.recordCheckResult(token1, true /* success */); + + // Check the expected storage state. No real way to tell if it has been updated, but + // we can check the final state is still what it should be. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions1); + + // Under the covers we expect it to fail to update because the storage should recognize that + // the token is no longer valid. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Peek inside the package tracker to make sure it is tracking failure counts properly. + assertEquals(1, mPackageTracker.getCheckFailureCountForTests()); + } + + @Test + public void trackingEnabled_reliabilityTrigger_firstTime_initialStorage() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + PackageVersions packageVersions = configureValidApplications(); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatusIsInitialOrReset(); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(packageVersions); + + // Confirm the token was correct. + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + assertEquals(packageVersions, token1.mPackageVersions); + + // token1 should be accepted. + mPackageTracker.recordCheckResult(token1, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions); + } + + @Test + public void trackingEnabled_reliabilityTrigger_afterRebootNoTriggerNeeded() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + PackageVersions packageVersions = configureValidApplications(); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did not attempt to trigger an update. + mFakeIntentHelper.assertUpdateNotTriggered(); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(packageVersions); + } + + /** + * Simulates the device starting where the storage records do not match the installed app + * versions. The reliability trigger should cause the package tracker to perform a check. + */ + @Test + public void trackingEnabled_reliabilityTrigger_afterRebootTriggerNeededBecausePreviousFailed() + throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + configureReliabilityConfigSettingsOk(); + + PackageVersions oldPackageVersions = new PackageVersions(1, 1); + PackageVersions currentPackageVersions = new PackageVersions(2, 2); + + // Simulate there being a newer version installed than the one recorded in storage. + configureValidApplications(currentPackageVersions); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(currentPackageVersions); + + // Simulate the update check completing successfully. + CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken(); + mPackageTracker.recordCheckResult(checkToken, true /* success */); + + // Check storage and reliability triggering state. + checkUpdateCheckSuccessful(currentPackageVersions); + } + + /** + * Simulates persistent failures of the reliability check. It should stop after the configured + * number of checks. + */ + @Test + public void trackingEnabled_reliabilityTrigger_repeatedFailures() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + + int retriesAllowed = 3; + int checkDelayMillis = 5 * 60 * 1000; + configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis); + + PackageVersions oldPackageVersions = new PackageVersions(1, 1); + PackageVersions currentPackageVersions = new PackageVersions(2, 2); + + // Simulate there being a newer version installed than the one recorded in storage. + configureValidApplications(currentPackageVersions); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + for (int i = 0; i < retriesAllowed + 1; i++) { + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(currentPackageVersions); + + // Check the PackageTracker failure count before calling recordCheckResult. + assertEquals(i, mPackageTracker.getCheckFailureCountForTests()); + + // Simulate a check failure. + CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken(); + mPackageTracker.recordCheckResult(checkToken, false /* success */); + + // Peek inside the package tracker to make sure it is tracking failure counts properly. + assertEquals(i + 1, mPackageTracker.getCheckFailureCountForTests()); + + // Confirm nothing has changed. + mFakeIntentHelper.assertUpdateNotTriggered(); + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, + currentPackageVersions); + + // Check reliability triggering is in the correct state. + if (i <= retriesAllowed) { + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + } else { + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + } + } + } + + @Test + public void trackingEnabled_reliabilityTrigger_failureCountIsReset() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + + int retriesAllowed = 3; + int checkDelayMillis = 5 * 60 * 1000; + configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis); + + PackageVersions oldPackageVersions = new PackageVersions(1, 1); + PackageVersions currentPackageVersions = new PackageVersions(2, 2); + + // Simulate there being a newer version installed than the one recorded in storage. + configureValidApplications(currentPackageVersions); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Fail (retries - 1) times. + for (int i = 0; i < retriesAllowed - 1; i++) { + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(currentPackageVersions); + + // Check the PackageTracker failure count before calling recordCheckResult. + assertEquals(i, mPackageTracker.getCheckFailureCountForTests()); + + // Simulate a check failure. + CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken(); + mPackageTracker.recordCheckResult(checkToken, false /* success */); + + // Peek inside the package tracker to make sure it is tracking failure counts properly. + assertEquals(i + 1, mPackageTracker.getCheckFailureCountForTests()); + + // Confirm nothing has changed. + mFakeIntentHelper.assertUpdateNotTriggered(); + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, + currentPackageVersions); + + // Check reliability triggering is still enabled. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + } + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(currentPackageVersions); + + // Check the PackageTracker failure count before calling recordCheckResult. + assertEquals(retriesAllowed - 1, mPackageTracker.getCheckFailureCountForTests()); + + // On the last possible try, succeed. + CheckToken checkToken = mFakeIntentHelper.captureAndResetLastToken(); + mPackageTracker.recordCheckResult(checkToken, true /* success */); + + checkUpdateCheckSuccessful(currentPackageVersions); + } + + /** + * Simulates reliability triggers happening too close together. Package tracker should ignore + * the ones it doesn't need. + */ + @Test + public void trackingEnabled_reliabilityTrigger_tooSoon() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + + int retriesAllowed = 5; + int checkDelayMillis = 5 * 60 * 1000; + configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis); + + PackageVersions oldPackageVersions = new PackageVersions(1, 1); + PackageVersions currentPackageVersions = new PackageVersions(2, 2); + + // Simulate there being a newer version installed than the one recorded in storage. + configureValidApplications(currentPackageVersions); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, oldPackageVersions); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(currentPackageVersions); + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + + // Increment the clock, but not enough. + mFakeClock.incrementClock(checkDelayMillis - 1); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did not trigger an update. + mFakeIntentHelper.assertUpdateNotTriggered(); + checkPackageStorageStatus(PackageStatus.CHECK_STARTED, currentPackageVersions); + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Increment the clock slightly more. Should now consider the response overdue. + mFakeClock.incrementClock(2); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Triggering should have happened. + checkUpdateCheckTriggered(currentPackageVersions); + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + + // Check a new token was generated. + assertFalse(token1.equals(token2)); + } + + /** + * Tests what happens when a package update doesn't complete and a reliability trigger cleans + * up for it. + */ + @Test + public void trackingEnabled_reliabilityTrigger_afterPackageUpdateDidNotComplete() + throws Exception { + + // Set up device configuration. + configureTrackingEnabled(); + + int retriesAllowed = 5; + int checkDelayMillis = 5 * 60 * 1000; + configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis); + + PackageVersions currentPackageVersions = new PackageVersions(1, 1); + PackageVersions newPackageVersions = new PackageVersions(2, 2); + + // Simulate there being a newer version installed than the one recorded in storage. + configureValidApplications(currentPackageVersions); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_SUCCESS, currentPackageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Simulate a reliability trigger. + simulatePackageInstallation(newPackageVersions); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(newPackageVersions); + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + + // Increment the clock, but not enough. + mFakeClock.incrementClock(checkDelayMillis + 1); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker triggered an update. + checkUpdateCheckTriggered(newPackageVersions); + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + + // Check a new token was generated. + assertFalse(token1.equals(token2)); + + // Simulate the reliability check completing. + mPackageTracker.recordCheckResult(token2, true /* success */); + + // Check everything is now as it should be. + checkUpdateCheckSuccessful(newPackageVersions); + } + + /** + * Simulates a reliability trigger happening too soon after a package update trigger occurred. + */ + @Test + public void trackingEnabled_reliabilityTriggerAfterUpdate_tooSoon() throws Exception { + // Set up device configuration. + configureTrackingEnabled(); + + int retriesAllowed = 5; + int checkDelayMillis = 5 * 60 * 1000; + configureReliabilityConfigSettings(retriesAllowed, checkDelayMillis); + + PackageVersions currentPackageVersions = new PackageVersions(1, 1); + PackageVersions newPackageVersions = new PackageVersions(2, 2); + + // Simulate there being a newer version installed than the one recorded in storage. + configureValidApplications(currentPackageVersions); + + // Force the storage into a state we want. + mPackageStatusStorage.forceCheckStateForTests( + PackageStatus.CHECK_COMPLETED_SUCCESS, currentPackageVersions); + + // Initialize the package tracker. + mPackageTracker.start(); + + // Check the intent helper is properly configured. + checkIntentHelperInitializedAndReliabilityTrackingEnabled(); + + // Check the initial storage state. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, currentPackageVersions); + + // Simulate a package update trigger. + simulatePackageInstallation(newPackageVersions); + + // Assert the PackageTracker did trigger an update. + checkUpdateCheckTriggered(newPackageVersions); + CheckToken token1 = mFakeIntentHelper.captureAndResetLastToken(); + + // Increment the clock, but not enough. + mFakeClock.incrementClock(checkDelayMillis - 1); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Assert the PackageTracker did not trigger an update. + mFakeIntentHelper.assertUpdateNotTriggered(); + checkPackageStorageStatus(PackageStatus.CHECK_STARTED, newPackageVersions); + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Increment the clock slightly more. Should now consider the response overdue. + mFakeClock.incrementClock(2); + + // Simulate a reliability trigger. + mFakeIntentHelper.simulateReliabilityTrigger(); + + // Triggering should have happened. + checkUpdateCheckTriggered(newPackageVersions); + CheckToken token2 = mFakeIntentHelper.captureAndResetLastToken(); + + // Check a new token was generated. + assertFalse(token1.equals(token2)); + } + + private void simulatePackageInstallation(PackageVersions packageVersions) throws Exception { + configureApplicationsValidManifests(packageVersions); + + // Simulate a tracked package being updated. + mFakeIntentHelper.simulatePackageUpdatedEvent(); + } + + /** + * Checks an update check was triggered, reliability triggering is therefore enabled and the + * storage state reflects that there is a check in progress. + */ + private void checkUpdateCheckTriggered(PackageVersions packageVersions) { + // Assert the PackageTracker attempted to trigger an update. + mFakeIntentHelper.assertUpdateTriggered(); + + // If an update check was triggered reliability triggering should always be enabled to + // ensure that it can be completed if it fails. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Check the expected storage state. + checkPackageStorageStatus(PackageStatus.CHECK_STARTED, packageVersions); + } + + private void checkUpdateCheckFailed(PackageVersions packageVersions) { + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + + // Assert the storage was updated. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_FAILURE, packageVersions); + } + + private void checkUpdateCheckSuccessful(PackageVersions packageVersions) { + // Check reliability triggering state. + mFakeIntentHelper.assertReliabilityTriggeringDisabled(); + + // Assert the storage was updated. + checkPackageStorageStatus(PackageStatus.CHECK_COMPLETED_SUCCESS, packageVersions); + + // Peek inside the package tracker to make sure it is tracking failure counts properly. + assertEquals(0, mPackageTracker.getCheckFailureCountForTests()); + } + + private PackageVersions configureValidApplications() throws Exception { + configureValidApplications(INITIAL_APP_PACKAGE_VERSIONS); + return INITIAL_APP_PACKAGE_VERSIONS; + } + + private void configureValidApplications(PackageVersions versions) throws Exception { + configureUpdateAppPackageOk(UPDATE_APP_PACKAGE_NAME); + configureDataAppPackageOk(DATA_APP_PACKAGE_NAME); + configureApplicationsValidManifests(versions); + } + + private void configureApplicationsValidManifests(PackageVersions versions) throws Exception { + configureUpdateAppManifestOk(UPDATE_APP_PACKAGE_NAME); + configureDataAppManifestOk(DATA_APP_PACKAGE_NAME); + configureUpdateAppPackageVersion(UPDATE_APP_PACKAGE_NAME, versions.mUpdateAppVersion); + configureDataAppPackageVersion(DATA_APP_PACKAGE_NAME, versions.mDataAppVersion); + } + + private void configureUpdateAppPackageVersion(String updateAppPackageName, + int updataAppPackageVersion) throws Exception { + when(mMockPackageManagerHelper.getInstalledPackageVersion(updateAppPackageName)) + .thenReturn(updataAppPackageVersion); + } + + private void configureDataAppPackageVersion(String dataAppPackageName, + int dataAppPackageVersion) throws Exception { + when(mMockPackageManagerHelper.getInstalledPackageVersion(dataAppPackageName)) + .thenReturn(dataAppPackageVersion); + } + + private void configureUpdateAppManifestOk(String updateAppPackageName) throws Exception { + Intent expectedIntent = RulesUpdaterContract.createUpdaterIntent(updateAppPackageName); + when(mMockPackageManagerHelper.receiverRegistered( + filterEquals(expectedIntent), + eq(RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION))) + .thenReturn(true); + when(mMockPackageManagerHelper.usesPermission( + updateAppPackageName, RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION)) + .thenReturn(true); + } + + private void configureUpdateAppManifestBad(String updateAppPackageName) throws Exception { + Intent expectedIntent = RulesUpdaterContract.createUpdaterIntent(updateAppPackageName); + when(mMockPackageManagerHelper.receiverRegistered( + filterEquals(expectedIntent), + eq(RulesUpdaterContract.TRIGGER_TIME_ZONE_RULES_CHECK_PERMISSION))) + .thenReturn(false); + // Has permission, but that shouldn't matter if the check above is false. + when(mMockPackageManagerHelper.usesPermission( + updateAppPackageName, RulesUpdaterContract.UPDATE_TIME_ZONE_RULES_PERMISSION)) + .thenReturn(true); + } + + private void configureDataAppManifestOk(String dataAppPackageName) throws Exception { + when(mMockPackageManagerHelper.contentProviderRegistered( + TimeZoneRulesDataContract.AUTHORITY, dataAppPackageName)) + .thenReturn(true); + } + + private void configureDataAppManifestBad(String dataAppPackageName) throws Exception { + // Simulate the data app not exposing the content provider we require. + when(mMockPackageManagerHelper.contentProviderRegistered( + TimeZoneRulesDataContract.AUTHORITY, dataAppPackageName)) + .thenReturn(false); + } + + private void configureTrackingEnabled() { + when(mMockConfigHelper.isTrackingEnabled()).thenReturn(true); + } + + private void configureTrackingDisabled() { + when(mMockConfigHelper.isTrackingEnabled()).thenReturn(false); + } + + private void configureReliabilityConfigSettings(int retriesAllowed, int checkDelayMillis) { + when(mMockConfigHelper.getFailedCheckRetryCount()).thenReturn(retriesAllowed); + when(mMockConfigHelper.getCheckTimeAllowedMillis()).thenReturn(checkDelayMillis); + } + + private void configureReliabilityConfigSettingsOk() { + configureReliabilityConfigSettings(5, 5 * 60 * 1000); + } + + private void configureUpdateAppPackageOk(String updateAppPackageName) throws Exception { + when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(updateAppPackageName); + when(mMockPackageManagerHelper.isPrivilegedApp(updateAppPackageName)).thenReturn(true); + } + + private void configureUpdateAppPackageNotPrivileged(String updateAppPackageName) + throws Exception { + when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(updateAppPackageName); + when(mMockPackageManagerHelper.isPrivilegedApp(updateAppPackageName)).thenReturn(false); + } + + private void configureUpdateAppPackageNameMissing() { + when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(null); + } + + private void configureDataAppPackageOk(String dataAppPackageName) throws Exception { + when(mMockConfigHelper.getDataAppPackageName()).thenReturn(dataAppPackageName); + when(mMockPackageManagerHelper.isPrivilegedApp(dataAppPackageName)).thenReturn(true); + } + + private void configureDataAppPackageNotPrivileged(String dataAppPackageName) + throws Exception { + when(mMockConfigHelper.getUpdateAppPackageName()).thenReturn(dataAppPackageName); + when(mMockPackageManagerHelper.isPrivilegedApp(dataAppPackageName)).thenReturn(false); + } + + private void configureDataAppPackageNameMissing() { + when(mMockConfigHelper.getDataAppPackageName()).thenThrow(new RuntimeException()); + } + + private void checkIntentHelperInitializedAndReliabilityTrackingEnabled() { + // Verify that calling start initialized the IntentHelper as well. + mFakeIntentHelper.assertInitialized(UPDATE_APP_PACKAGE_NAME, DATA_APP_PACKAGE_NAME); + + // Assert that reliability tracking is always enabled after initialization. + mFakeIntentHelper.assertReliabilityTriggeringEnabled(); + } + + private void checkPackageStorageStatus( + int expectedCheckStatus, PackageVersions expectedPackageVersions) { + PackageStatus packageStatus = mPackageStatusStorage.getPackageStatus(); + assertEquals(expectedCheckStatus, packageStatus.mCheckStatus); + assertEquals(expectedPackageVersions, packageStatus.mVersions); + } + + private void checkPackageStorageStatusIsInitialOrReset() { + assertNull(mPackageStatusStorage.getPackageStatus()); + } + + private static CheckToken createArbitraryCheckToken() { + return new CheckToken(1, INITIAL_APP_PACKAGE_VERSIONS); + } + + /** + * A fake IntentHelper implementation for use in tests. + */ + private static class FakeIntentHelper implements IntentHelper { + + private Listener mListener; + private String mUpdateAppPackageName; + private String mDataAppPackageName; + + private CheckToken mLastToken; + + private boolean mReliabilityTriggeringEnabled; + + @Override + public void initialize(String updateAppPackageName, String dataAppPackageName, + Listener listener) { + assertNotNull(updateAppPackageName); + assertNotNull(dataAppPackageName); + assertNotNull(listener); + mListener = listener; + mUpdateAppPackageName = updateAppPackageName; + mDataAppPackageName = dataAppPackageName; + } + + public void assertInitialized( + String expectedUpdateAppPackageName, String expectedDataAppPackageName) { + assertNotNull(mListener); + assertEquals(expectedUpdateAppPackageName, mUpdateAppPackageName); + assertEquals(expectedDataAppPackageName, mDataAppPackageName); + } + + public void assertNotInitialized() { + assertNull(mListener); + } + + @Override + public void sendTriggerUpdateCheck(CheckToken checkToken) { + if (mLastToken != null) { + fail("lastToken already set"); + } + mLastToken = checkToken; + } + + @Override + public void enableReliabilityTriggering() { + mReliabilityTriggeringEnabled = true; + } + + @Override + public void disableReliabilityTriggering() { + mReliabilityTriggeringEnabled = false; + } + + public void assertReliabilityTriggeringEnabled() { + assertTrue(mReliabilityTriggeringEnabled); + } + + public void assertReliabilityTriggeringDisabled() { + assertFalse(mReliabilityTriggeringEnabled); + } + + public void assertUpdateTriggered() { + assertNotNull(mLastToken); + } + + public void assertUpdateNotTriggered() { + assertNull(mLastToken); + } + + public CheckToken captureAndResetLastToken() { + CheckToken toReturn = mLastToken; + assertNotNull("No update triggered", toReturn); + mLastToken = null; + return toReturn; + } + + public void simulatePackageUpdatedEvent() { + mListener.triggerUpdateIfNeeded(true); + } + + public void simulateReliabilityTrigger() { + mListener.triggerUpdateIfNeeded(false); + } + } + + private static class FakeClockHelper implements ClockHelper { + + private long currentTime = 1000; + + @Override + public long currentTimestamp() { + return currentTime; + } + + public void incrementClock(long millis) { + currentTime += millis; + } + } + + /** + * Registers a mockito parameter matcher that uses {@link Intent#filterEquals(Intent)}. to + * check the parameter against the intent supplied. + */ + private static Intent filterEquals(final Intent expected) { + final Matcher<Intent> m = new BaseMatcher<Intent>() { + @Override + public boolean matches(Object actual) { + return actual != null && expected.filterEquals((Intent) actual); + } + @Override + public void describeTo(Description description) { + description.appendText(expected.toString()); + } + }; + return argThat(m); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageVersionsTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageVersionsTest.java new file mode 100644 index 000000000000..a470f8f6c230 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezone/PackageVersionsTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import org.junit.Test; + +import android.support.test.filters.SmallTest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SmallTest +public class PackageVersionsTest { + + @Test + public void equals() { + PackageVersions baseline = + new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */); + assertEquals(baseline, baseline); + + PackageVersions deepEqual = + new PackageVersions(1 /* updateAppVersion */, 1 /* dataAppVersion */); + assertEquals(baseline, deepEqual); + + PackageVersions differentUpdateAppVersion = + new PackageVersions(2 /* updateAppVersion */, 1 /* dataAppVersion */); + assertFalse(baseline.equals(differentUpdateAppVersion)); + + PackageVersions differentDataAppVersion = + new PackageVersions(1 /* updateAppVersion */, 2 /* dataAppVersion */); + assertFalse(baseline.equals(differentDataAppVersion)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java new file mode 100644 index 000000000000..a7f4c9947482 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezone/RulesManagerServiceTest.java @@ -0,0 +1,924 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezone; + +import org.junit.Before; +import org.junit.Test; + +import android.app.timezone.Callback; +import android.app.timezone.DistroRulesVersion; +import android.app.timezone.ICallback; +import android.app.timezone.RulesManager; +import android.app.timezone.RulesState; +import android.os.ParcelFileDescriptor; + +import java.io.IOException; +import java.util.concurrent.Executor; +import javax.annotation.Nullable; +import libcore.tzdata.shared2.DistroVersion; +import libcore.tzdata.shared2.StagedDistroOperation; +import libcore.tzdata.update2.TimeZoneDistroInstaller; + +import static com.android.server.timezone.RulesManagerService.REQUIRED_UPDATER_PERMISSION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * White box interaction / unit testing of the {@link RulesManagerService}. + */ +public class RulesManagerServiceTest { + + private RulesManagerService mRulesManagerService; + + private FakeExecutor mFakeExecutor; + private PermissionHelper mMockPermissionHelper; + private FileDescriptorHelper mMockFileDescriptorHelper; + private PackageTracker mMockPackageTracker; + private TimeZoneDistroInstaller mMockTimeZoneDistroInstaller; + + @Before + public void setUp() { + mFakeExecutor = new FakeExecutor(); + + mMockFileDescriptorHelper = mock(FileDescriptorHelper.class); + mMockPackageTracker = mock(PackageTracker.class); + mMockPermissionHelper = mock(PermissionHelper.class); + mMockTimeZoneDistroInstaller = mock(TimeZoneDistroInstaller.class); + + mRulesManagerService = new RulesManagerService( + mMockPermissionHelper, + mFakeExecutor, + mMockFileDescriptorHelper, + mMockPackageTracker, + mMockTimeZoneDistroInstaller); + } + + @Test(expected = SecurityException.class) + public void getRulesState_noCallerPermission() throws Exception { + configureCallerDoesNotHavePermission(); + mRulesManagerService.getRulesState(); + } + + @Test(expected = SecurityException.class) + public void requestInstall_noCallerPermission() throws Exception { + configureCallerDoesNotHavePermission(); + mRulesManagerService.requestInstall(null, null, null); + } + + @Test(expected = SecurityException.class) + public void requestUninstall_noCallerPermission() throws Exception { + configureCallerDoesNotHavePermission(); + mRulesManagerService.requestUninstall(null, null); + } + + @Test(expected = SecurityException.class) + public void requestNothing_noCallerPermission() throws Exception { + configureCallerDoesNotHavePermission(); + mRulesManagerService.requestNothing(null, true); + } + + @Test + public void getRulesState_systemRulesError() throws Exception { + configureDeviceCannotReadSystemRulesVersion(); + + assertNull(mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_stagedInstall() throws Exception { + configureCallerHasPermission(); + + configureDeviceSystemRulesVersion("2016a"); + + DistroVersion stagedDistroVersion = new DistroVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1, + "2016c", + 3); + configureStagedInstall(stagedDistroVersion); + + DistroVersion installedDistroVersion = new DistroVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1, + "2016b", + 4); + configureInstalledDistroVersion(installedDistroVersion); + + DistroRulesVersion stagedDistroRulesVersion = new DistroRulesVersion( + stagedDistroVersion.rulesVersion, stagedDistroVersion.revision); + DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion( + installedDistroVersion.rulesVersion, installedDistroVersion.revision); + RulesState expectedRuleState = new RulesState( + "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + false /* operationInProgress */, + RulesState.STAGED_OPERATION_INSTALL, stagedDistroRulesVersion, + RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_nothingStaged() throws Exception { + configureCallerHasPermission(); + + configureDeviceSystemRulesVersion("2016a"); + + configureNoStagedOperation(); + + DistroVersion installedDistroVersion = new DistroVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1, + "2016b", + 4); + configureInstalledDistroVersion(installedDistroVersion); + + DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion( + installedDistroVersion.rulesVersion, installedDistroVersion.revision); + RulesState expectedRuleState = new RulesState( + "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + false /* operationInProgress */, + RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */, + RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_uninstallStaged() throws Exception { + configureCallerHasPermission(); + + configureDeviceSystemRulesVersion("2016a"); + + configureStagedUninstall(); + + DistroVersion installedDistroVersion = new DistroVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1, + "2016b", + 4); + configureInstalledDistroVersion(installedDistroVersion); + + DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion( + installedDistroVersion.rulesVersion, installedDistroVersion.revision); + RulesState expectedRuleState = new RulesState( + "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + false /* operationInProgress */, + RulesState.STAGED_OPERATION_UNINSTALL, null /* stagedDistroRulesVersion */, + RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_installedRulesError() throws Exception { + configureCallerHasPermission(); + + String systemRulesVersion = "2016a"; + configureDeviceSystemRulesVersion(systemRulesVersion); + + configureStagedUninstall(); + configureDeviceCannotReadInstalledDistroVersion(); + + RulesState expectedRuleState = new RulesState( + "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + false /* operationInProgress */, + RulesState.STAGED_OPERATION_UNINSTALL, null /* stagedDistroRulesVersion */, + RulesState.DISTRO_STATUS_UNKNOWN, null /* installedDistroRulesVersion */); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_stagedRulesError() throws Exception { + configureCallerHasPermission(); + + String systemRulesVersion = "2016a"; + configureDeviceSystemRulesVersion(systemRulesVersion); + + configureDeviceCannotReadStagedDistroOperation(); + + DistroVersion installedDistroVersion = new DistroVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1, + "2016b", + 4); + configureInstalledDistroVersion(installedDistroVersion); + + DistroRulesVersion installedDistroRulesVersion = new DistroRulesVersion( + installedDistroVersion.rulesVersion, installedDistroVersion.revision); + RulesState expectedRuleState = new RulesState( + "2016a", RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + false /* operationInProgress */, + RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */, + RulesState.DISTRO_STATUS_INSTALLED, installedDistroRulesVersion); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_noInstalledRules() throws Exception { + configureCallerHasPermission(); + + String systemRulesVersion = "2016a"; + configureDeviceSystemRulesVersion(systemRulesVersion); + configureNoStagedOperation(); + configureInstalledDistroVersion(null); + + RulesState expectedRuleState = new RulesState( + systemRulesVersion, RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + false /* operationInProgress */, + RulesState.STAGED_OPERATION_NONE, null /* stagedDistroRulesVersion */, + RulesState.DISTRO_STATUS_NONE, null /* installedDistroRulesVersion */); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void getRulesState_operationInProgress() throws Exception { + configureCallerHasPermission(); + + String systemRulesVersion = "2016a"; + String installedRulesVersion = "2016b"; + int revision = 3; + + configureDeviceSystemRulesVersion(systemRulesVersion); + + DistroVersion installedDistroVersion = new DistroVersion( + DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, + DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1, + installedRulesVersion, + revision); + configureInstalledDistroVersion(installedDistroVersion); + + byte[] expectedContent = createArbitraryBytes(1000); + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent); + + // Start an async operation so there is one in progress. The mFakeExecutor won't actually + // execute it. + byte[] tokenBytes = createArbitraryTokenBytes(); + ICallback callback = new StubbedCallback(); + + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback); + + RulesState expectedRuleState = new RulesState( + systemRulesVersion, RulesManagerService.DISTRO_FORMAT_VERSION_SUPPORTED, + true /* operationInProgress */, + RulesState.STAGED_OPERATION_UNKNOWN, null /* stagedDistroRulesVersion */, + RulesState.DISTRO_STATUS_UNKNOWN, null /* installedDistroRulesVersion */); + assertEquals(expectedRuleState, mRulesManagerService.getRulesState()); + } + + @Test + public void requestInstall_operationInProgress() throws Exception { + configureCallerHasPermission(); + + byte[] expectedContent = createArbitraryBytes(1000); + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent); + + byte[] tokenBytes = createArbitraryTokenBytes(); + ICallback callback = new StubbedCallback(); + + // First request should succeed. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback)); + + // Something async should be enqueued. Clear it but do not execute it so we can detect the + // second request does nothing. + mFakeExecutor.getAndResetLastCommand(); + + // Second request should fail. + assertEquals(RulesManager.ERROR_OPERATION_IN_PROGRESS, + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback)); + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestInstall_badToken() throws Exception { + configureCallerHasPermission(); + + byte[] expectedContent = createArbitraryBytes(1000); + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent); + + byte[] badTokenBytes = new byte[2]; + ICallback callback = new StubbedCallback(); + + try { + mRulesManagerService.requestInstall(parcelFileDescriptor, badTokenBytes, callback); + fail(); + } catch (IllegalArgumentException expected) { + } + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestInstall_nullParcelFileDescriptor() throws Exception { + configureCallerHasPermission(); + + ParcelFileDescriptor parcelFileDescriptor = null; + byte[] tokenBytes = createArbitraryTokenBytes(); + ICallback callback = new StubbedCallback(); + + try { + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback); + fail(); + } catch (NullPointerException expected) {} + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestInstall_nullCallback() throws Exception { + configureCallerHasPermission(); + + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + byte[] tokenBytes = createArbitraryTokenBytes(); + ICallback callback = null; + + try { + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback); + fail(); + } catch (NullPointerException expected) {} + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestInstall_asyncSuccess() throws Exception { + configureCallerHasPermission(); + + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + byte[] expectedContent = createArbitraryBytes(1000); + configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + TestCallback callback = new TestCallback(); + + // Request the install. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback)); + + // Assert nothing has happened yet. + callback.assertNoResultReceived(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + + // Set up the installer. + configureStageInstallExpectation(expectedContent, TimeZoneDistroInstaller.INSTALL_SUCCESS); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify the expected calls were made to other components. + verifyStageInstallCalled(expectedContent); + verifyPackageTrackerCalled(token, true /* success */); + + // Check the callback was called. + callback.assertResultReceived(Callback.SUCCESS); + } + + @Test + public void requestInstall_nullTokenBytes() throws Exception { + configureCallerHasPermission(); + + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + byte[] expectedContent = createArbitraryBytes(1000); + configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent); + + TestCallback callback = new TestCallback(); + + // Request the install. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestInstall( + parcelFileDescriptor, null /* tokenBytes */, callback)); + + // Assert nothing has happened yet. + verifyNoInstallerCallsMade(); + callback.assertNoResultReceived(); + + // Set up the installer. + configureStageInstallExpectation(expectedContent, TimeZoneDistroInstaller.INSTALL_SUCCESS); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify the expected calls were made to other components. + verifyStageInstallCalled(expectedContent); + verifyPackageTrackerCalled(null /* expectedToken */, true /* success */); + + // Check the callback was received. + callback.assertResultReceived(Callback.SUCCESS); + } + + @Test + public void requestInstall_asyncInstallFail() throws Exception { + configureCallerHasPermission(); + + byte[] expectedContent = createArbitraryBytes(1000); + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + configureParcelFileDescriptorReadSuccess(parcelFileDescriptor, expectedContent); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + TestCallback callback = new TestCallback(); + + // Request the install. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback)); + + // Assert nothing has happened yet. + verifyNoInstallerCallsMade(); + callback.assertNoResultReceived(); + + // Set up the installer. + configureStageInstallExpectation( + expectedContent, TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify the expected calls were made to other components. + verifyStageInstallCalled(expectedContent); + + // Validation failure is treated like a successful check: repeating it won't improve things. + boolean expectedSuccess = true; + verifyPackageTrackerCalled(token, expectedSuccess); + + // Check the callback was received. + callback.assertResultReceived(Callback.ERROR_INSTALL_VALIDATION_ERROR); + } + + @Test + public void requestInstall_asyncParcelFileDescriptorReadFail() throws Exception { + configureCallerHasPermission(); + + ParcelFileDescriptor parcelFileDescriptor = createFakeParcelFileDescriptor(); + configureParcelFileDescriptorReadFailure(parcelFileDescriptor); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + TestCallback callback = new TestCallback(); + + // Request the install. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestInstall(parcelFileDescriptor, tokenBytes, callback)); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify nothing else happened. + verifyNoInstallerCallsMade(); + + // A failure to read the ParcelFileDescriptor is treated as a failure. It might be the + // result of a file system error. This is a fairly arbitrary choice. + verifyPackageTrackerCalled(token, false /* success */); + + verifyNoPackageTrackerCallsMade(); + + // Check the callback was received. + callback.assertResultReceived(Callback.ERROR_UNKNOWN_FAILURE); + } + + @Test + public void requestUninstall_operationInProgress() throws Exception { + configureCallerHasPermission(); + + byte[] tokenBytes = createArbitraryTokenBytes(); + ICallback callback = new StubbedCallback(); + + // First request should succeed. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestUninstall(tokenBytes, callback)); + + // Something async should be enqueued. Clear it but do not execute it so we can detect the + // second request does nothing. + mFakeExecutor.getAndResetLastCommand(); + + // Second request should fail. + assertEquals(RulesManager.ERROR_OPERATION_IN_PROGRESS, + mRulesManagerService.requestUninstall(tokenBytes, callback)); + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestUninstall_badToken() throws Exception { + configureCallerHasPermission(); + + byte[] badTokenBytes = new byte[2]; + ICallback callback = new StubbedCallback(); + + try { + mRulesManagerService.requestUninstall(badTokenBytes, callback); + fail(); + } catch (IllegalArgumentException expected) { + } + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestUninstall_nullCallback() throws Exception { + configureCallerHasPermission(); + + byte[] tokenBytes = createArbitraryTokenBytes(); + ICallback callback = null; + + try { + mRulesManagerService.requestUninstall(tokenBytes, callback); + fail(); + } catch (NullPointerException expected) {} + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestUninstall_asyncSuccess() throws Exception { + configureCallerHasPermission(); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + TestCallback callback = new TestCallback(); + + // Request the uninstall. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestUninstall(tokenBytes, callback)); + + // Assert nothing has happened yet. + callback.assertNoResultReceived(); + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + + // Set up the installer. + configureStageUninstallExpectation(true /* success */); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify the expected calls were made to other components. + verifyStageUninstallCalled(); + verifyPackageTrackerCalled(token, true /* success */); + + // Check the callback was called. + callback.assertResultReceived(Callback.SUCCESS); + } + + @Test + public void requestUninstall_nullTokenBytes() throws Exception { + configureCallerHasPermission(); + + TestCallback callback = new TestCallback(); + + // Request the uninstall. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestUninstall(null /* tokenBytes */, callback)); + + // Assert nothing has happened yet. + verifyNoInstallerCallsMade(); + callback.assertNoResultReceived(); + + // Set up the installer. + configureStageUninstallExpectation(true /* success */); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify the expected calls were made to other components. + verifyStageUninstallCalled(); + verifyPackageTrackerCalled(null /* expectedToken */, true /* success */); + + // Check the callback was received. + callback.assertResultReceived(Callback.SUCCESS); + } + + @Test + public void requestUninstall_asyncUninstallFail() throws Exception { + configureCallerHasPermission(); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + TestCallback callback = new TestCallback(); + + // Request the uninstall. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestUninstall(tokenBytes, callback)); + + // Assert nothing has happened yet. + verifyNoInstallerCallsMade(); + callback.assertNoResultReceived(); + + // Set up the installer. + configureStageUninstallExpectation(false /* success */); + + // Simulate the async execution. + mFakeExecutor.simulateAsyncExecutionOfLastCommand(); + + // Verify the expected calls were made to other components. + verifyStageUninstallCalled(); + verifyPackageTrackerCalled(token, false /* success */); + + // Check the callback was received. + callback.assertResultReceived(Callback.ERROR_UNKNOWN_FAILURE); + } + + @Test + public void requestNothing_operationInProgressOk() throws Exception { + configureCallerHasPermission(); + + // Set up a parallel operation. + assertEquals(RulesManager.SUCCESS, + mRulesManagerService.requestUninstall(null, new StubbedCallback())); + // Something async should be enqueued. Clear it but do not execute it to simulate it still + // being in progress. + mFakeExecutor.getAndResetLastCommand(); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + // Make the call. + mRulesManagerService.requestNothing(tokenBytes, true /* success */); + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + + // Verify the expected calls were made to other components. + verifyPackageTrackerCalled(token, true /* success */); + verifyNoInstallerCallsMade(); + } + + @Test + public void requestNothing_badToken() throws Exception { + configureCallerHasPermission(); + + byte[] badTokenBytes = new byte[2]; + + try { + mRulesManagerService.requestNothing(badTokenBytes, true /* success */); + fail(); + } catch (IllegalArgumentException expected) { + } + + // Assert nothing async was enqueued. + mFakeExecutor.assertNothingQueued(); + + // Assert no other calls were made. + verifyNoInstallerCallsMade(); + verifyNoPackageTrackerCallsMade(); + } + + @Test + public void requestNothing() throws Exception { + configureCallerHasPermission(); + + CheckToken token = createArbitraryToken(); + byte[] tokenBytes = token.toByteArray(); + + // Make the call. + mRulesManagerService.requestNothing(tokenBytes, false /* success */); + + // Assert everything required was done. + verifyNoInstallerCallsMade(); + verifyPackageTrackerCalled(token, false /* success */); + } + + @Test + public void requestNothing_nullTokenBytes() throws Exception { + configureCallerHasPermission(); + + // Make the call. + mRulesManagerService.requestNothing(null /* tokenBytes */, true /* success */); + + // Assert everything required was done. + verifyNoInstallerCallsMade(); + verifyPackageTrackerCalled(null /* token */, true /* success */); + } + + private void verifyNoPackageTrackerCallsMade() { + verifyNoMoreInteractions(mMockPackageTracker); + reset(mMockPackageTracker); + } + + private void verifyPackageTrackerCalled( + CheckToken expectedCheckToken, boolean expectedSuccess) { + verify(mMockPackageTracker).recordCheckResult(expectedCheckToken, expectedSuccess); + reset(mMockPackageTracker); + } + + private void configureCallerHasPermission() throws Exception { + doNothing() + .when(mMockPermissionHelper) + .enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); + } + + private void configureCallerDoesNotHavePermission() { + doThrow(new SecurityException("Simulated permission failure")) + .when(mMockPermissionHelper) + .enforceCallerHasPermission(REQUIRED_UPDATER_PERMISSION); + } + + private void configureParcelFileDescriptorReadSuccess(ParcelFileDescriptor parcelFileDescriptor, + byte[] content) throws Exception { + when(mMockFileDescriptorHelper.readFully(parcelFileDescriptor)).thenReturn(content); + } + + private void configureParcelFileDescriptorReadFailure(ParcelFileDescriptor parcelFileDescriptor) + throws Exception { + when(mMockFileDescriptorHelper.readFully(parcelFileDescriptor)) + .thenThrow(new IOException("Simulated failure")); + } + + private void configureStageInstallExpectation(byte[] expectedContent, int resultCode) + throws Exception { + when(mMockTimeZoneDistroInstaller.stageInstallWithErrorCode(eq(expectedContent))) + .thenReturn(resultCode); + } + + private void configureStageUninstallExpectation(boolean success) throws Exception { + doReturn(success).when(mMockTimeZoneDistroInstaller).stageUninstall(); + } + + private void verifyStageInstallCalled(byte[] expectedContent) throws Exception { + verify(mMockTimeZoneDistroInstaller).stageInstallWithErrorCode(eq(expectedContent)); + verifyNoMoreInteractions(mMockTimeZoneDistroInstaller); + reset(mMockTimeZoneDistroInstaller); + } + + private void verifyStageUninstallCalled() throws Exception { + verify(mMockTimeZoneDistroInstaller).stageUninstall(); + verifyNoMoreInteractions(mMockTimeZoneDistroInstaller); + reset(mMockTimeZoneDistroInstaller); + } + + private void verifyNoInstallerCallsMade() { + verifyNoMoreInteractions(mMockTimeZoneDistroInstaller); + reset(mMockTimeZoneDistroInstaller); + } + + private static byte[] createArbitraryBytes(int length) { + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + bytes[i] = (byte) i; + } + return bytes; + } + + private byte[] createArbitraryTokenBytes() { + return createArbitraryToken().toByteArray(); + } + + private CheckToken createArbitraryToken() { + return new CheckToken(1, new PackageVersions(1, 1)); + } + + private ParcelFileDescriptor createFakeParcelFileDescriptor() { + return new ParcelFileDescriptor((ParcelFileDescriptor) null); + } + + private void configureDeviceSystemRulesVersion(String systemRulesVersion) throws Exception { + when(mMockTimeZoneDistroInstaller.getSystemRulesVersion()).thenReturn(systemRulesVersion); + } + + private void configureInstalledDistroVersion(@Nullable DistroVersion installedDistroVersion) + throws Exception { + when(mMockTimeZoneDistroInstaller.getInstalledDistroVersion()) + .thenReturn(installedDistroVersion); + } + + private void configureStagedInstall(DistroVersion stagedDistroVersion) throws Exception { + when(mMockTimeZoneDistroInstaller.getStagedDistroOperation()) + .thenReturn(StagedDistroOperation.install(stagedDistroVersion)); + } + + private void configureStagedUninstall() throws Exception { + when(mMockTimeZoneDistroInstaller.getStagedDistroOperation()) + .thenReturn(StagedDistroOperation.uninstall()); + } + + private void configureNoStagedOperation() throws Exception { + when(mMockTimeZoneDistroInstaller.getStagedDistroOperation()).thenReturn(null); + } + + private void configureDeviceCannotReadStagedDistroOperation() throws Exception { + when(mMockTimeZoneDistroInstaller.getStagedDistroOperation()) + .thenThrow(new IOException("Simulated failure")); + } + + private void configureDeviceCannotReadSystemRulesVersion() throws Exception { + when(mMockTimeZoneDistroInstaller.getSystemRulesVersion()) + .thenThrow(new IOException("Simulated failure")); + } + + private void configureDeviceCannotReadInstalledDistroVersion() throws Exception { + when(mMockTimeZoneDistroInstaller.getInstalledDistroVersion()) + .thenThrow(new IOException("Simulated failure")); + } + + private static class FakeExecutor implements Executor { + + private Runnable mLastCommand; + + @Override + public void execute(Runnable command) { + assertNull(mLastCommand); + assertNotNull(command); + mLastCommand = command; + } + + public Runnable getAndResetLastCommand() { + assertNotNull(mLastCommand); + Runnable toReturn = mLastCommand; + mLastCommand = null; + return toReturn; + } + + public void simulateAsyncExecutionOfLastCommand() { + Runnable toRun = getAndResetLastCommand(); + toRun.run(); + } + + public void assertNothingQueued() { + assertNull(mLastCommand); + } + } + + private static class TestCallback extends ICallback.Stub { + + private boolean mOnFinishedCalled; + private int mLastError; + + @Override + public void onFinished(int error) { + assertFalse(mOnFinishedCalled); + mOnFinishedCalled = true; + mLastError = error; + } + + public void assertResultReceived(int expectedResult) { + assertTrue(mOnFinishedCalled); + assertEquals(expectedResult, mLastError); + } + + public void assertNoResultReceived() { + assertFalse(mOnFinishedCalled); + } + } + + private static class StubbedCallback extends ICallback.Stub { + @Override + public void onFinished(int error) { + fail("Unexpected call"); + } + } +} |