Merge "Add (disabled) time zone update system server impl"

This commit is contained in:
Treehugger Robot
2017-05-05 14:06:23 +00:00
committed by Gerrit Code Review
25 changed files with 4824 additions and 0 deletions

View File

@@ -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());
}});
}
/**

View File

@@ -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

View File

@@ -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" />

View File

@@ -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 +
'}';
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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");
}
}
}