Merge "Add (disabled) time zone update system server impl"
This commit is contained in:
@@ -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());
|
||||
}});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user