diff options
| author | 2016-12-02 11:21:45 +0000 | |
|---|---|---|
| committer | 2017-01-17 15:06:22 +0000 | |
| commit | d65799ee812132bc54fd9d67d4ef9b19906b9da5 (patch) | |
| tree | cf64fd04e10b531329146fec58498d078ed9d917 | |
| parent | 0872c2455e896c9238014efb4d739c62a8103b74 (diff) | |
Store pending OTA state and make it accessible via polling api.
Change-Id: Ieb71dfb902371a683b17561f51ba9c2c730eb37b
Test: gts-tradefed run gts -a armeabi-v7a -m GtsGmscoreHostTestCases -t com.google.android.gts.devicepolicy.DeviceOwnerTest
Bug: 31000521
| -rw-r--r-- | api/current.txt | 8 | ||||
| -rw-r--r-- | api/system-current.txt | 8 | ||||
| -rw-r--r-- | api/test-current.txt | 8 | ||||
| -rw-r--r-- | core/java/android/app/admin/DevicePolicyManager.java | 21 | ||||
| -rw-r--r-- | core/java/android/app/admin/IDevicePolicyManager.aidl | 2 | ||||
| -rw-r--r-- | core/java/android/app/admin/SystemUpdateInfo.aidl | 20 | ||||
| -rw-r--r-- | core/java/android/app/admin/SystemUpdateInfo.java | 128 | ||||
| -rw-r--r-- | services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java | 48 | ||||
| -rw-r--r-- | services/devicepolicy/java/com/android/server/devicepolicy/Owners.java | 61 |
9 files changed, 273 insertions, 31 deletions
diff --git a/api/current.txt b/api/current.txt index 6da09e35fe5e..dfb33025cd6a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6107,6 +6107,7 @@ package android.app.admin { method public int getPasswordMinimumSymbols(android.content.ComponentName); method public int getPasswordMinimumUpperCase(android.content.ComponentName); method public int getPasswordQuality(android.content.ComponentName); + method public android.app.admin.SystemUpdateInfo getPendingSystemUpdate(android.content.ComponentName); method public int getPermissionGrantState(android.content.ComponentName, java.lang.String, java.lang.String); method public int getPermissionPolicy(android.content.ComponentName); method public java.util.List<java.lang.String> getPermittedAccessibilityServices(android.content.ComponentName); @@ -6328,6 +6329,13 @@ package android.app.admin { field public static final android.os.Parcelable.Creator<android.app.admin.SecurityLog.SecurityEvent> CREATOR; } + public final class SystemUpdateInfo implements android.os.Parcelable { + method public int describeContents(); + method public long getReceivedTime(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.admin.SystemUpdateInfo> CREATOR; + } + public class SystemUpdatePolicy implements android.os.Parcelable { method public static android.app.admin.SystemUpdatePolicy createAutomaticInstallPolicy(); method public static android.app.admin.SystemUpdatePolicy createPostponeInstallPolicy(); diff --git a/api/system-current.txt b/api/system-current.txt index 5640eafcee1f..dfe57d871e41 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6292,6 +6292,7 @@ package android.app.admin { method public int getPasswordMinimumSymbols(android.content.ComponentName); method public int getPasswordMinimumUpperCase(android.content.ComponentName); method public int getPasswordQuality(android.content.ComponentName); + method public android.app.admin.SystemUpdateInfo getPendingSystemUpdate(android.content.ComponentName); method public int getPermissionGrantState(android.content.ComponentName, java.lang.String, java.lang.String); method public int getPermissionPolicy(android.content.ComponentName); method public java.util.List<java.lang.String> getPermittedAccessibilityServices(android.content.ComponentName); @@ -6537,6 +6538,13 @@ package android.app.admin { field public static final android.os.Parcelable.Creator<android.app.admin.SecurityLog.SecurityEvent> CREATOR; } + public final class SystemUpdateInfo implements android.os.Parcelable { + method public int describeContents(); + method public long getReceivedTime(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.admin.SystemUpdateInfo> CREATOR; + } + public class SystemUpdatePolicy implements android.os.Parcelable { method public static android.app.admin.SystemUpdatePolicy createAutomaticInstallPolicy(); method public static android.app.admin.SystemUpdatePolicy createPostponeInstallPolicy(); diff --git a/api/test-current.txt b/api/test-current.txt index 714cac9fc559..189c6e2ab79f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -6128,6 +6128,7 @@ package android.app.admin { method public int getPasswordMinimumSymbols(android.content.ComponentName); method public int getPasswordMinimumUpperCase(android.content.ComponentName); method public int getPasswordQuality(android.content.ComponentName); + method public android.app.admin.SystemUpdateInfo getPendingSystemUpdate(android.content.ComponentName); method public int getPermissionGrantState(android.content.ComponentName, java.lang.String, java.lang.String); method public int getPermissionPolicy(android.content.ComponentName); method public java.util.List<java.lang.String> getPermittedAccessibilityServices(android.content.ComponentName); @@ -6350,6 +6351,13 @@ package android.app.admin { field public static final android.os.Parcelable.Creator<android.app.admin.SecurityLog.SecurityEvent> CREATOR; } + public final class SystemUpdateInfo implements android.os.Parcelable { + method public int describeContents(); + method public long getReceivedTime(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.app.admin.SystemUpdateInfo> CREATOR; + } + public class SystemUpdatePolicy implements android.os.Parcelable { method public static android.app.admin.SystemUpdatePolicy createAutomaticInstallPolicy(); method public static android.app.admin.SystemUpdatePolicy createPostponeInstallPolicy(); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index c95e0113e801..6d6ada6ddab9 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -20,7 +20,6 @@ import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; @@ -28,7 +27,6 @@ import android.annotation.TestApi; import android.annotation.UserIdInt; import android.annotation.WorkerThread; import android.app.Activity; -import android.app.admin.PasswordMetrics; import android.app.IServiceConnection; import android.app.admin.SecurityLog.SecurityEvent; import android.content.ComponentName; @@ -5536,7 +5534,7 @@ public class DevicePolicyManager { * {@link DevicePolicyManager#setApplicationRestrictions} was called, or an empty * {@link Bundle} if no restrictions have been set. * @throws SecurityException if {@code admin} is not a device or profile owner. - * @see {@link #setApplicationRestrictionsManagingPackage} + * @see #setApplicationRestrictionsManagingPackage */ @WorkerThread public @NonNull Bundle getApplicationRestrictions( @@ -6221,6 +6219,23 @@ public class DevicePolicyManager { } /** + * Called by device or profile owners to get information about a pending system update. + * + * @param admin Which profile or device owner this request is associated with. + * @return Information about a pending system update or {@code null} if no update pending. + * @throws SecurityException if {@code admin} is not a device or profile owner. + * @see DeviceAdminReceiver#onSystemUpdatePending(Context, Intent, long) + */ + public @Nullable SystemUpdateInfo getPendingSystemUpdate(@NonNull ComponentName admin) { + throwIfParentInstance("getPendingSystemUpdate"); + try { + return mService.getPendingSystemUpdate(admin); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Called by profile or device owners to set the default response for future runtime permission * requests by applications. The policy can allow for normal operation which prompts the user to * grant a permission, or can allow automatic granting or denying of runtime permission requests diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 66185d53fb0a..8891f93fcbb4 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -20,6 +20,7 @@ package android.app.admin; import android.app.admin.NetworkEvent; import android.app.IApplicationThread; import android.app.IServiceConnection; +import android.app.admin.SystemUpdateInfo; import android.app.admin.SystemUpdatePolicy; import android.app.admin.PasswordMetrics; import android.content.ComponentName; @@ -264,6 +265,7 @@ interface IDevicePolicyManager { boolean getDoNotAskCredentialsOnBoot(); void notifyPendingSystemUpdate(in long updateReceivedTime); + SystemUpdateInfo getPendingSystemUpdate(in ComponentName admin); void setPermissionPolicy(in ComponentName admin, int policy); int getPermissionPolicy(in ComponentName admin); diff --git a/core/java/android/app/admin/SystemUpdateInfo.aidl b/core/java/android/app/admin/SystemUpdateInfo.aidl new file mode 100644 index 000000000000..6d14904f4a1d --- /dev/null +++ b/core/java/android/app/admin/SystemUpdateInfo.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 2017, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.app.admin; + +parcelable SystemUpdateInfo; diff --git a/core/java/android/app/admin/SystemUpdateInfo.java b/core/java/android/app/admin/SystemUpdateInfo.java new file mode 100644 index 000000000000..0937f3c37c59 --- /dev/null +++ b/core/java/android/app/admin/SystemUpdateInfo.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.admin; + +import android.annotation.Nullable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Objects; + +/** + * A class containing information about a pending system update. + */ +public final class SystemUpdateInfo implements Parcelable { + private static final String ATTR_RECEIVED_TIME = "mReceivedTime"; + // Tag used to store original build fingerprint to detect when the update is applied. + private static final String ATTR_ORIGINAL_BUILD = "originalBuild"; + private final long mReceivedTime; + + private SystemUpdateInfo(long receivedTime) { + this.mReceivedTime = receivedTime; + } + + private SystemUpdateInfo(Parcel in) { + mReceivedTime = in.readLong(); + } + + /** + * @hide + */ + @Nullable + public static SystemUpdateInfo of(long receivedTime) { + return receivedTime == -1 ? null : new SystemUpdateInfo(receivedTime); + } + + /** + * Get time when the update was first available. + * @return time as given by {@link System#currentTimeMillis()} + */ + public long getReceivedTime() { + return mReceivedTime; + } + + public static final Creator<SystemUpdateInfo> CREATOR = + new Creator<SystemUpdateInfo>() { + @Override + public SystemUpdateInfo createFromParcel(Parcel in) { + return new SystemUpdateInfo(in); + } + + @Override + public SystemUpdateInfo[] newArray(int size) { + return new SystemUpdateInfo[size]; + } + }; + + /** + * @hide + */ + public void writeToXml(XmlSerializer out, String tag) throws IOException { + out.startTag(null, tag); + out.attribute(null, ATTR_RECEIVED_TIME, String.valueOf(mReceivedTime)); + out.attribute(null, ATTR_ORIGINAL_BUILD , Build.FINGERPRINT); + out.endTag(null, tag); + } + + /** + * @hide + */ + @Nullable + public static SystemUpdateInfo readFromXml(XmlPullParser parser) { + // If an OTA has been applied (build fingerprint has changed), discard stale info. + final String buildFingerprint = parser.getAttributeValue(null, ATTR_ORIGINAL_BUILD ); + if (!Build.FINGERPRINT.equals(buildFingerprint)) { + return null; + } + final long receivedTime = + Long.parseLong(parser.getAttributeValue(null, ATTR_RECEIVED_TIME)); + return of(receivedTime); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(getReceivedTime()); + } + + @Override + public String toString() { + return String.format("SystemUpdateInfo (receivedTime = %d)", mReceivedTime); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SystemUpdateInfo that = (SystemUpdateInfo) o; + return mReceivedTime == that.mReceivedTime; + } + + @Override + public int hashCode() { + return Objects.hash(mReceivedTime); + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 040188dded2c..76bab4f6badc 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -69,6 +69,7 @@ import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.IDevicePolicyManager; import android.app.admin.NetworkEvent; import android.app.admin.PasswordMetrics; +import android.app.admin.SystemUpdateInfo; import android.app.admin.SecurityLog; import android.app.admin.SecurityLog.SecurityEvent; import android.app.admin.SystemUpdatePolicy; @@ -4526,9 +4527,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null); } } else { - synchronized (this) { - getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - } + enforceProfileOrDeviceOwner(who); + } + } + + private void enforceProfileOrDeviceOwner(ComponentName who) { + synchronized (this) { + getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); } } @@ -4538,9 +4543,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { throw new SecurityException("who == null, but caller is not cert installer"); } } else { - synchronized (this) { - getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - } + enforceProfileOrDeviceOwner(who); } } @@ -4830,9 +4833,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage, boolean lockdown) throws SecurityException { - synchronized (this) { - getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - } + enforceProfileOrDeviceOwner(admin); final int userId = mInjector.userHandleGetCallingUserId(); final long token = mInjector.binderClearCallingIdentity(); @@ -4854,9 +4855,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public String getAlwaysOnVpnPackage(ComponentName admin) throws SecurityException { - synchronized (this) { - getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - } + enforceProfileOrDeviceOwner(admin); final int userId = mInjector.userHandleGetCallingUserId(); final long token = mInjector.binderClearCallingIdentity(); @@ -6999,9 +6998,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private void enforceCanManageApplicationRestrictions(ComponentName who) { if (who != null) { - synchronized (this) { - getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - } + enforceProfileOrDeviceOwner(who); } else if (!isCallerApplicationRestrictionsManagingPackage()) { throw new SecurityException( "No admin component given, and caller cannot manage application restrictions " @@ -8801,9 +8798,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "can broadcast update information."); return; } + + if (!mOwners.saveSystemUpdateInfo(updateReceivedTime)) { + // Received time hasn't changed, don't send duplicate notification. + return; + } + final Intent intent = new Intent(DeviceAdminReceiver.ACTION_NOTIFY_PENDING_SYSTEM_UPDATE); - intent.putExtra(DeviceAdminReceiver.EXTRA_SYSTEM_UPDATE_RECEIVED_TIME, - updateReceivedTime); + intent.putExtra(DeviceAdminReceiver.EXTRA_SYSTEM_UPDATE_RECEIVED_TIME, updateReceivedTime); final long ident = mInjector.binderClearCallingIdentity(); try { @@ -8842,6 +8844,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override + public SystemUpdateInfo getPendingSystemUpdate(ComponentName admin) { + Preconditions.checkNotNull(admin, "ComponentName is null"); + enforceProfileOrDeviceOwner(admin); + + return mOwners.getSystemUpdateInfo(); + } + + @Override public void setPermissionPolicy(ComponentName admin, int policy) throws RemoteException { int userId = UserHandle.getCallingUserId(); synchronized (this) { @@ -9168,9 +9178,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public boolean isManagedProfile(ComponentName admin) { - synchronized (this) { - getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER); - } + enforceProfileOrDeviceOwner(admin); return isManagedProfile(mInjector.userHandleGetCallingUserId()); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index b53933e07f2b..99c76b169598 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import android.annotation.Nullable; +import android.app.admin.SystemUpdateInfo; import android.app.admin.SystemUpdatePolicy; import android.content.ComponentName; import android.content.pm.PackageManagerInternal; @@ -47,13 +48,14 @@ import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import libcore.io.IoUtils; /** - * Stores and restores state for the Device and Profile owners. By definition there can be - * only one device owner, but there may be a profile owner for each user. + * Stores and restores state for the Device and Profile owners and related device-wide information. + * By definition there can be only one device owner, but there may be a profile owner for each user. * * <p>This class is thread safe, so individual methods can safely be called without locking. * However, caller must still synchronize on their side to ensure integrity between multiple calls. @@ -65,6 +67,7 @@ class Owners { private static final String DEVICE_OWNER_XML_LEGACY = "device_owner.xml"; + // XML storing device owner info, system update policy and pending OTA update information. private static final String DEVICE_OWNER_XML = "device_owner_2.xml"; private static final String PROFILE_OWNER_XML = "profile_owner.xml"; @@ -73,6 +76,8 @@ class Owners { private static final String TAG_DEVICE_OWNER = "device-owner"; private static final String TAG_DEVICE_INITIALIZER = "device-initializer"; + private static final String TAG_SYSTEM_UPDATE_POLICY = "system-update-policy"; + private static final String TAG_PENDING_OTA_INFO = "pending-ota-info"; private static final String TAG_PROFILE_OWNER = "profile-owner"; // Holds "context" for device-owner, this must not be show up before device-owner. private static final String TAG_DEVICE_OWNER_CONTEXT = "device-owner-context"; @@ -85,8 +90,6 @@ class Owners { private static final String ATTR_USERID = "userId"; private static final String ATTR_USER_RESTRICTIONS_MIGRATED = "userRestrictionsMigrated"; - private static final String TAG_SYSTEM_UPDATE_POLICY = "system-update-policy"; - private final UserManager mUserManager; private final UserManagerInternal mUserManagerInternal; private final PackageManagerInternal mPackageManagerInternal; @@ -102,6 +105,10 @@ class Owners { // Local system update policy controllable by device owner. private SystemUpdatePolicy mSystemUpdatePolicy; + // Pending OTA info if there is one. + @Nullable + private SystemUpdateInfo mSystemUpdateInfo; + private final Object mLock = new Object(); public Owners(UserManager userManager, @@ -468,6 +475,31 @@ class Owners { } } + /** + * @return Whether update received time has changed. + */ + boolean saveSystemUpdateInfo(long receivedTime) { + final SystemUpdateInfo newSystemUpdateInfo = SystemUpdateInfo.of(receivedTime); + synchronized (mLock) { + // Check if we already have the same update information. + if (Objects.equals(newSystemUpdateInfo, mSystemUpdateInfo)) { + return false; + } + + mSystemUpdateInfo = newSystemUpdateInfo; + new DeviceOwnerReadWriter().writeToFileLocked(); + + return true; + } + } + + @Nullable + public SystemUpdateInfo getSystemUpdateInfo() { + synchronized (mLock) { + return mSystemUpdateInfo; + } + } + private abstract static class FileReadWriter { private final File mFile; @@ -573,7 +605,7 @@ class Owners { } } } catch (XmlPullParserException | IOException e) { - Slog.e(TAG, "Error parsing device-owner file", e); + Slog.e(TAG, "Error parsing owners information file", e); } finally { IoUtils.closeQuietly(input); } @@ -592,7 +624,8 @@ class Owners { @Override boolean shouldWrite() { - return (mDeviceOwner != null) || (mSystemUpdatePolicy != null); + return (mDeviceOwner != null) || (mSystemUpdatePolicy != null) + || (mSystemUpdateInfo != null); } @Override @@ -609,6 +642,10 @@ class Owners { mSystemUpdatePolicy.saveToXml(out); out.endTag(null, TAG_SYSTEM_UPDATE_POLICY); } + + if (mSystemUpdateInfo != null) { + mSystemUpdateInfo.writeToXml(out, TAG_PENDING_OTA_INFO); + } } @Override @@ -637,6 +674,9 @@ class Owners { case TAG_SYSTEM_UPDATE_POLICY: mSystemUpdatePolicy = SystemUpdatePolicy.restoreFromXml(parser); break; + case TAG_PENDING_OTA_INFO: + mSystemUpdateInfo = SystemUpdateInfo.readFromXml(parser); + break; default: Slog.e(TAG, "Unexpected tag: " + tag); return false; @@ -783,7 +823,6 @@ class Owners { } if (mSystemUpdatePolicy != null) { if (needBlank) { - needBlank = false; pw.println(); } pw.println(prefix + "System Update Policy: " + mSystemUpdatePolicy); @@ -792,7 +831,6 @@ class Owners { if (mProfileOwners != null) { for (Map.Entry<Integer, OwnerInfo> entry : mProfileOwners.entrySet()) { if (needBlank) { - needBlank = false; pw.println(); } pw.println(prefix + "Profile Owner (User " + entry.getKey() + "): "); @@ -800,6 +838,13 @@ class Owners { needBlank = true; } } + if (mSystemUpdateInfo != null) { + if (needBlank) { + pw.println(); + } + pw.println(prefix + "Pending System Update: " + mSystemUpdateInfo); + needBlank = true; + } } File getLegacyConfigFileWithTestOverride() { |