From 19f291660d825c099e3039d0b9149538ffac8fe6 Mon Sep 17 00:00:00 2001 From: Neda Topoljanac Date: Mon, 22 Oct 2018 18:12:16 +0100 Subject: [PATCH] Managed System Updates API Adding API to install a system update from a file on the device. Test: manual in TestDPC, CTS tests for negative cases: atest com.android.cts.devicepolicy.DeviceOwnerTest#testInstallUpdate Fixes: 116511569 Change-Id: I34b5c6344301a9d2d64c98dedc4ed5e4a75c57d1 --- Android.bp | 1 + api/current.txt | 11 + api/system-current.txt | 4 +- .../app/admin/DevicePolicyManager.java | 101 ++++++- .../app/admin/IDevicePolicyManager.aidl | 3 + .../admin/StartInstallingUpdateCallback.aidl | 27 ++ .../devicepolicy/AbUpdateInstaller.java | 268 ++++++++++++++++++ .../BaseIDevicePolicyManager.java | 6 + .../devicepolicy/DevicePolicyConstants.java | 25 ++ .../DevicePolicyManagerService.java | 29 ++ .../devicepolicy/NonAbUpdateInstaller.java | 52 ++++ .../server/devicepolicy/UpdateInstaller.java | 146 ++++++++++ 12 files changed, 670 insertions(+), 3 deletions(-) create mode 100644 core/java/android/app/admin/StartInstallingUpdateCallback.aidl create mode 100644 services/devicepolicy/java/com/android/server/devicepolicy/AbUpdateInstaller.java create mode 100644 services/devicepolicy/java/com/android/server/devicepolicy/NonAbUpdateInstaller.java create mode 100644 services/devicepolicy/java/com/android/server/devicepolicy/UpdateInstaller.java diff --git a/Android.bp b/Android.bp index 7e038ce8d3ae..f40aab15e26e 100644 --- a/Android.bp +++ b/Android.bp @@ -92,6 +92,7 @@ java_defaults { "core/java/android/app/IWallpaperManagerCallback.aidl", "core/java/android/app/admin/IDeviceAdminService.aidl", "core/java/android/app/admin/IDevicePolicyManager.aidl", + "core/java/android/app/admin/StartInstallingUpdateCallback.aidl", "core/java/android/app/trust/IStrongAuthTracker.aidl", "core/java/android/app/trust/ITrustManager.aidl", "core/java/android/app/trust/ITrustListener.aidl", diff --git a/api/current.txt b/api/current.txt index e84bc8db67da..abd3c312e7e6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6598,6 +6598,7 @@ package android.app.admin { method public boolean installKeyPair(android.content.ComponentName, java.security.PrivateKey, java.security.cert.Certificate, java.lang.String); method public boolean installKeyPair(android.content.ComponentName, java.security.PrivateKey, java.security.cert.Certificate[], java.lang.String, boolean); method public boolean installKeyPair(android.content.ComponentName, java.security.PrivateKey, java.security.cert.Certificate[], java.lang.String, int); + method public void installSystemUpdate(android.content.ComponentName, android.net.Uri, java.util.concurrent.Executor, android.app.admin.DevicePolicyManager.InstallUpdateCallback); method public boolean isActivePasswordSufficient(); method public boolean isAdminActive(android.content.ComponentName); method public boolean isAffiliatedUser(); @@ -6840,6 +6841,16 @@ package android.app.admin { field public static final int WIPE_RESET_PROTECTION_DATA = 2; // 0x2 } + public static abstract class DevicePolicyManager.InstallUpdateCallback { + ctor public DevicePolicyManager.InstallUpdateCallback(); + method public void onInstallUpdateError(int, java.lang.String); + field public static final int UPDATE_ERROR_BATTERY_LOW = 5; // 0x5 + field public static final int UPDATE_ERROR_FILE_NOT_FOUND = 4; // 0x4 + field public static final int UPDATE_ERROR_INCORRECT_OS_VERSION = 2; // 0x2 + field public static final int UPDATE_ERROR_UNKNOWN = 1; // 0x1 + field public static final int UPDATE_ERROR_UPDATE_FILE_INVALID = 3; // 0x3 + } + public static abstract interface DevicePolicyManager.OnClearApplicationUserDataListener { method public abstract void onApplicationUserDataCleared(java.lang.String, boolean); } diff --git a/api/system-current.txt b/api/system-current.txt index 0d8ed4487216..4760de790e45 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -441,10 +441,10 @@ package android.app { } public class KeyguardManager { - method public void setPrivateNotificationsAllowed(boolean); - method public boolean getPrivateNotificationsAllowed(); method public android.content.Intent createConfirmFactoryResetCredentialIntent(java.lang.CharSequence, java.lang.CharSequence, java.lang.CharSequence); + method public boolean getPrivateNotificationsAllowed(); method public void requestDismissKeyguard(android.app.Activity, java.lang.CharSequence, android.app.KeyguardManager.KeyguardDismissCallback); + method public void setPrivateNotificationsAllowed(boolean); } public class Notification implements android.os.Parcelable { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 24ee7f757f55..00c1863a1ef6 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -53,6 +53,7 @@ import android.net.ProxyInfo; import android.net.Uri; import android.os.Binder; import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.PersistableBundle; import android.os.Process; @@ -87,6 +88,7 @@ import com.android.internal.util.Preconditions; import com.android.org.conscrypt.TrustedCertificateStore; import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1929,6 +1931,48 @@ public class DevicePolicyManager { */ public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; + /** + * Callback used in {@link #installSystemUpdate} to indicate that there was an error while + * trying to install an update. + */ + public abstract static class InstallUpdateCallback { + /** Represents an unknown error while trying to install an update. */ + public static final int UPDATE_ERROR_UNKNOWN = 1; + + /** Represents the update file being intended for different OS version. */ + public static final int UPDATE_ERROR_INCORRECT_OS_VERSION = 2; + + /** + * Represents the update file being wrong, i.e. payloads are mismatched, wrong compressions + * method. + */ + public static final int UPDATE_ERROR_UPDATE_FILE_INVALID = 3; + + /** Represents that the file could not be found. */ + public static final int UPDATE_ERROR_FILE_NOT_FOUND = 4; + + /** Represents the battery being too low to apply an update. */ + public static final int UPDATE_ERROR_BATTERY_LOW = 5; + + /** Method invoked when there was an error while installing an update. */ + public void onInstallUpdateError( + @InstallUpdateCallbackErrorConstants int errorCode, String errorMessage) { + } + } + + /** + * @hide + */ + @IntDef(prefix = { "UPDATE_ERROR_" }, value = { + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN, + InstallUpdateCallback.UPDATE_ERROR_INCORRECT_OS_VERSION, + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID, + InstallUpdateCallback.UPDATE_ERROR_FILE_NOT_FOUND, + InstallUpdateCallback.UPDATE_ERROR_BATTERY_LOW + }) + @Retention(RetentionPolicy.SOURCE) + public @interface InstallUpdateCallbackErrorConstants {} + /** * Return true if the given administrator component is currently active (enabled) in the system. * @@ -6796,7 +6840,6 @@ public class DevicePolicyManager { @Retention(RetentionPolicy.SOURCE) public @interface CreateAndManageUserFlags {} - /** * Called by a device owner to create a user with the specified name and a given component of * the calling package as profile owner. The UserHandle returned by this method should not be @@ -9826,6 +9869,62 @@ public class DevicePolicyManager { } } + /** + * Called by device owner to install a system update from the given file. The device will be + * rebooted in order to finish installing the update. Note that if the device is rebooted, this + * doesn't necessarily mean that the update has been applied successfully. The caller should + * additionally check the system version with {@link android.os.Build#FINGERPRINT} or {@link + * android.os.Build.VERSION}. If an error occurs during processing the OTA before the reboot, + * the caller will be notified by {@link InstallUpdateCallback}. If device does not have + * sufficient battery level, the installation will fail with error {@link + * InstallUpdateCallback#UPDATE_ERROR_BATTERY_LOW}. + * + * @param admin The {@link DeviceAdminReceiver} that this request is associated with. + * @param updateFilePath An Uri of the file that contains the update. The file should be + * readable by the calling app. + * @param executor The executor through which the callback should be invoked. + * @param callback A callback object that will inform the caller when installing an update + * fails. + */ + public void installSystemUpdate( + @NonNull ComponentName admin, @NonNull Uri updateFilePath, + @NonNull @CallbackExecutor Executor executor, + @NonNull InstallUpdateCallback callback) { + throwIfParentInstance("installUpdate"); + if (mService == null) { + return; + } + try (ParcelFileDescriptor fileDescriptor = mContext.getContentResolver() + .openFileDescriptor(updateFilePath, "r")) { + mService.installUpdateFromFile( + admin, fileDescriptor, new StartInstallingUpdateCallback.Stub() { + @Override + public void onStartInstallingUpdateError( + int errorCode, String errorMessage) { + executeCallback(errorCode, errorMessage, executor, callback); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (FileNotFoundException e) { + Log.w(TAG, e); + executeCallback( + InstallUpdateCallback.UPDATE_ERROR_FILE_NOT_FOUND, Log.getStackTraceString(e), + executor, callback); + } catch (IOException e) { + Log.w(TAG, e); + executeCallback( + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN, Log.getStackTraceString(e), + executor, callback); + } + } + + private void executeCallback(int errorCode, String errorMessage, + @NonNull @CallbackExecutor Executor executor, + @NonNull InstallUpdateCallback callback) { + executor.execute(() -> callback.onInstallUpdateError(errorCode, errorMessage)); + } + /** * Returns the system-wide Private DNS mode. * diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 918c1278a2fe..60f79d62873b 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.StartInstallingUpdateCallback; import android.app.admin.SystemUpdateInfo; import android.app.admin.SystemUpdatePolicy; import android.app.admin.PasswordMetrics; @@ -419,4 +420,6 @@ interface IDevicePolicyManager { String getGlobalPrivateDnsHost(in ComponentName admin); void grantDeviceIdsAccessToProfileOwner(in ComponentName who, int userId); + + void installUpdateFromFile(in ComponentName admin, in ParcelFileDescriptor updateFileDescriptor, in StartInstallingUpdateCallback listener); } diff --git a/core/java/android/app/admin/StartInstallingUpdateCallback.aidl b/core/java/android/app/admin/StartInstallingUpdateCallback.aidl new file mode 100644 index 000000000000..df04707e4446 --- /dev/null +++ b/core/java/android/app/admin/StartInstallingUpdateCallback.aidl @@ -0,0 +1,27 @@ +/* +** +** Copyright 2018, 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; + +/** +* Callback used between {@link DevicePolicyManager} and {@link DevicePolicyManagerService} to +* indicate that starting installing an update is finished. +* {@hide} +*/ +oneway interface StartInstallingUpdateCallback { + void onStartInstallingUpdateError(int errorCode, String errorMessage); +} \ No newline at end of file diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/AbUpdateInstaller.java b/services/devicepolicy/java/com/android/server/devicepolicy/AbUpdateInstaller.java new file mode 100644 index 000000000000..05912a5e3776 --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/AbUpdateInstaller.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2018 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.devicepolicy; + +import android.app.admin.DevicePolicyManager.InstallUpdateCallback; +import android.app.admin.StartInstallingUpdateCallback; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.os.UpdateEngine; +import android.os.UpdateEngineCallback; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +/** + * Used for installing an update on AB + * devices. + *

This logic is specific to GOTA and should be modified by OEMs using a different AB update + * system.

+ */ +class AbUpdateInstaller extends UpdateInstaller { + private static final String PAYLOAD_BIN = "payload.bin"; + private static final String PAYLOAD_PROPERTIES_TXT = "payload_properties.txt"; + //https://en.wikipedia.org/wiki/Zip_(file_format)#Local_file_header + private static final int OFFSET_TO_FILE_NAME = 30; + // kDownloadStateInitializationError constant from system/update_engine/common/error_code.h. + private static final int DOWNLOAD_STATE_INITIALIZATION_ERROR = 20; + private long mSizeForUpdate; + private long mOffsetForUpdate; + private List mProperties; + private Enumeration mEntries; + private ZipFile mPackedUpdateFile; + private static final Map errorCodesMap = buildErrorCodesMap(); + private static final Map errorStringsMap = buildErrorStringsMap(); + public static final String UNKNOWN_ERROR = "Unknown error with error code = "; + private boolean mUpdateInstalled; + + private static Map buildErrorCodesMap() { + Map map = new HashMap<>(); + map.put(UpdateEngine.ErrorCodeConstants.ERROR, InstallUpdateCallback.UPDATE_ERROR_UNKNOWN); + map.put( + DOWNLOAD_STATE_INITIALIZATION_ERROR, + InstallUpdateCallback.UPDATE_ERROR_INCORRECT_OS_VERSION); + + // Error constants corresponding to errors related to bad update file. + map.put( + UpdateEngine.ErrorCodeConstants.DOWNLOAD_PAYLOAD_VERIFICATION_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID); + map.put( + UpdateEngine.ErrorCodeConstants.PAYLOAD_SIZE_MISMATCH_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID); + map.put( + UpdateEngine.ErrorCodeConstants.PAYLOAD_MISMATCHED_TYPE_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID); + map.put( + UpdateEngine.ErrorCodeConstants.PAYLOAD_HASH_MISMATCH_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID); + + // Error constants corresponding to errors related to devices bad state. + map.put( + UpdateEngine.ErrorCodeConstants.POST_INSTALL_RUNNER_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN); + map.put( + UpdateEngine.ErrorCodeConstants.INSTALL_DEVICE_OPEN_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN); + map.put( + UpdateEngine.ErrorCodeConstants.DOWNLOAD_TRANSFER_ERROR, + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN); + map.put( + UpdateEngine.ErrorCodeConstants.UPDATED_BUT_NOT_ACTIVE, + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN); + + return map; + } + + private static Map buildErrorStringsMap() { + Map map = new HashMap<>(); + map.put(UpdateEngine.ErrorCodeConstants.ERROR, UNKNOWN_ERROR); + map.put( + DOWNLOAD_STATE_INITIALIZATION_ERROR, + "The delta update payload was targeted for another version or the source partition" + + "was modified after it was installed"); + map.put( + UpdateEngine.ErrorCodeConstants.POST_INSTALL_RUNNER_ERROR, + "Failed to finish the configured postinstall works."); + map.put( + UpdateEngine.ErrorCodeConstants.INSTALL_DEVICE_OPEN_ERROR, + "Failed to open one of the partitions it tried to write to or read data from."); + map.put( + UpdateEngine.ErrorCodeConstants.PAYLOAD_MISMATCHED_TYPE_ERROR, + "Payload mismatch error."); + map.put( + UpdateEngine.ErrorCodeConstants.DOWNLOAD_TRANSFER_ERROR, + "Failed to read the payload data from the given URL."); + map.put( + UpdateEngine.ErrorCodeConstants.PAYLOAD_HASH_MISMATCH_ERROR, "Payload hash error."); + map.put( + UpdateEngine.ErrorCodeConstants.PAYLOAD_SIZE_MISMATCH_ERROR, + "Payload size mismatch error."); + map.put( + UpdateEngine.ErrorCodeConstants.DOWNLOAD_PAYLOAD_VERIFICATION_ERROR, + "Failed to verify the signature of the payload."); + map.put( + UpdateEngine.ErrorCodeConstants.UPDATED_BUT_NOT_ACTIVE, + "The payload has been successfully installed," + + "but the active slot was not flipped."); + return map; + } + + AbUpdateInstaller(Context context, ParcelFileDescriptor updateFileDescriptor, + StartInstallingUpdateCallback callback, DevicePolicyManagerService.Injector injector, + DevicePolicyConstants constants) { + super(context, updateFileDescriptor, callback, injector, constants); + mUpdateInstalled = false; + } + + @Override + public void installUpdateInThread() { + if (mUpdateInstalled) { + throw new IllegalStateException("installUpdateInThread can be called only once."); + } + try { + setState(); + applyPayload(Paths.get(mCopiedUpdateFile.getAbsolutePath()).toUri().toString()); + } catch (ZipException e) { + Log.w(UpdateInstaller.TAG, e); + notifyCallbackOnError( + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID, + Log.getStackTraceString(e)); + } catch (IOException e) { + Log.w(UpdateInstaller.TAG, e); + notifyCallbackOnError( + InstallUpdateCallback.UPDATE_ERROR_UNKNOWN, Log.getStackTraceString(e)); + } + } + + private void setState() throws IOException { + mUpdateInstalled = true; + mPackedUpdateFile = new ZipFile(mCopiedUpdateFile); + mProperties = new ArrayList<>(); + mSizeForUpdate = -1; + mOffsetForUpdate = 0; + mEntries = mPackedUpdateFile.entries(); + } + + private UpdateEngine buildBoundUpdateEngine() { + UpdateEngine updateEngine = new UpdateEngine(); + updateEngine.bind(new DelegatingUpdateEngineCallback(this, updateEngine)); + return updateEngine; + } + + private void applyPayload(String updatePath) throws IOException { + if (!updateStateForPayload()) { + return; + } + String[] headerKeyValuePairs = mProperties.stream().toArray(String[]::new); + if (mSizeForUpdate == -1) { + Log.w(UpdateInstaller.TAG, "Failed to find payload entry in the given package."); + notifyCallbackOnError( + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID, + "Failed to find payload entry in the given package."); + return; + } + + UpdateEngine updateEngine = buildBoundUpdateEngine(); + updateEngine.applyPayload( + updatePath, mOffsetForUpdate, mSizeForUpdate, headerKeyValuePairs); + } + + private boolean updateStateForPayload() throws IOException { + long offset = 0; + while (mEntries.hasMoreElements()) { + ZipEntry entry = mEntries.nextElement(); + + String name = entry.getName(); + offset += buildOffsetForEntry(entry, name); + if (entry.isDirectory()) { + offset -= entry.getCompressedSize(); + continue; + } + if (PAYLOAD_BIN.equals(name)) { + if (entry.getMethod() != ZipEntry.STORED) { + Log.w(UpdateInstaller.TAG, "Invalid compression method."); + notifyCallbackOnError( + InstallUpdateCallback.UPDATE_ERROR_UPDATE_FILE_INVALID, + "Invalid compression method."); + return false; + } + mSizeForUpdate = entry.getCompressedSize(); + mOffsetForUpdate = offset - entry.getCompressedSize(); + } else if (PAYLOAD_PROPERTIES_TXT.equals(name)) { + updatePropertiesForEntry(entry); + } + } + return true; + } + + private long buildOffsetForEntry(ZipEntry entry, String name) { + return OFFSET_TO_FILE_NAME + name.length() + entry.getCompressedSize() + + (entry.getExtra() == null ? 0 : entry.getExtra().length); + } + + private void updatePropertiesForEntry(ZipEntry entry) throws IOException { + try (BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(mPackedUpdateFile.getInputStream(entry)))) { + String line; + /* Neither @line nor @mProperties are size constraint since there is a few properties + with limited size. */ + while ((line = bufferedReader.readLine()) != null) { + mProperties.add(line); + } + } + } + + private static class DelegatingUpdateEngineCallback extends UpdateEngineCallback { + private UpdateInstaller mUpdateInstaller; + private UpdateEngine mUpdateEngine; + + DelegatingUpdateEngineCallback( + UpdateInstaller updateInstaller, UpdateEngine updateEngine) { + mUpdateInstaller = updateInstaller; + mUpdateEngine = updateEngine; + } + + @Override + public void onStatusUpdate(int statusCode, float percentage) { + return; + } + + @Override + public void onPayloadApplicationComplete(int errorCode) { + mUpdateEngine.unbind(); + if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS) { + mUpdateInstaller.notifyCallbackOnSuccess(); + } else { + mUpdateInstaller.notifyCallbackOnError( + errorCodesMap.getOrDefault( + errorCode, InstallUpdateCallback.UPDATE_ERROR_UNKNOWN), + errorStringsMap.getOrDefault(errorCode, UNKNOWN_ERROR + errorCode)); + } + } + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java index 5926bddbf847..6462d163cc9d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java @@ -17,7 +17,9 @@ package com.android.server.devicepolicy; import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; +import android.app.admin.StartInstallingUpdateCallback; import android.content.ComponentName; +import android.os.ParcelFileDescriptor; import com.android.server.SystemService; @@ -91,4 +93,8 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { @Override public void grantDeviceIdsAccessToProfileOwner(ComponentName who, int userId) { } + + @Override + public void installUpdateFromFile(ComponentName admin, + ParcelFileDescriptor updateFileDescriptor, StartInstallingUpdateCallback listener) {} } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java index 71fea02c282f..fd59b4328f86 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyConstants.java @@ -42,6 +42,12 @@ public class DevicePolicyConstants { private static final String DAS_DIED_SERVICE_STABLE_CONNECTION_THRESHOLD_SEC_KEY = "das_died_service_stable_connection_threshold_sec"; + private static final String BATTERY_THRESHOLD_NOT_CHARGING_KEY = + "battery_threshold_not_charging"; + + private static final String BATTERY_THRESHOLD_CHARGING_KEY = + "battery_threshold_charging"; + /** * The back-off before re-connecting, when a service binding died, due to the owner * crashing repeatedly. @@ -63,6 +69,17 @@ public class DevicePolicyConstants { */ public final long DAS_DIED_SERVICE_STABLE_CONNECTION_THRESHOLD_SEC; + /** + * Battery threshold for installing system update while the device is not charging. + */ + public final int BATTERY_THRESHOLD_NOT_CHARGING; + + /** + * Battery threshold for installing system update while the device is charging. + */ + public final int BATTERY_THRESHOLD_CHARGING; + + private DevicePolicyConstants(String settings) { final KeyValueListParser parser = new KeyValueListParser(','); @@ -87,6 +104,12 @@ public class DevicePolicyConstants { DAS_DIED_SERVICE_STABLE_CONNECTION_THRESHOLD_SEC_KEY, TimeUnit.MINUTES.toSeconds(2)); + int batteryThresholdNotCharging = parser.getInt( + BATTERY_THRESHOLD_NOT_CHARGING_KEY, 40); + + int batteryThresholdCharging = parser.getInt( + BATTERY_THRESHOLD_CHARGING_KEY, 20); + // Set minimum: 5 seconds. dasDiedServiceReconnectBackoffSec = Math.max(5, dasDiedServiceReconnectBackoffSec); @@ -103,6 +126,8 @@ public class DevicePolicyConstants { DAS_DIED_SERVICE_RECONNECT_MAX_BACKOFF_SEC = dasDiedServiceReconnectMaxBackoffSec; DAS_DIED_SERVICE_STABLE_CONNECTION_THRESHOLD_SEC = dasDiedServiceStableConnectionThresholdSec; + BATTERY_THRESHOLD_NOT_CHARGING = batteryThresholdNotCharging; + BATTERY_THRESHOLD_CHARGING = batteryThresholdCharging; } public static DevicePolicyConstants loadFromString(String settings) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index bbbc40cedd3e..7751b4a44b88 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -113,6 +113,7 @@ import android.app.admin.NetworkEvent; import android.app.admin.PasswordMetrics; import android.app.admin.SecurityLog; import android.app.admin.SecurityLog.SecurityEvent; +import android.app.admin.StartInstallingUpdateCallback; import android.app.admin.SystemUpdateInfo; import android.app.admin.SystemUpdatePolicy; import android.app.backup.IBackupManager; @@ -379,6 +380,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private static final Set GLOBAL_SETTINGS_DEPRECATED; private static final Set SYSTEM_SETTINGS_WHITELIST; private static final Set DA_DISALLOWED_POLICIES; + private static final String AB_DEVICE_KEY = "ro.build.ab_update"; + static { SECURE_SETTINGS_WHITELIST = new ArraySet<>(); SECURE_SETTINGS_WHITELIST.add(Settings.Secure.DEFAULT_INPUT_METHOD); @@ -13315,4 +13318,30 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return mInjector.settingsGlobalGetString(PRIVATE_DNS_SPECIFIER); } + + @Override + public void installUpdateFromFile(ComponentName admin, + ParcelFileDescriptor updateFileDescriptor, StartInstallingUpdateCallback callback) { + enforceDeviceOwner(admin); + final long id = mInjector.binderClearCallingIdentity(); + try { + UpdateInstaller updateInstaller; + if (isDeviceAB()) { + updateInstaller = new AbUpdateInstaller( + mContext, updateFileDescriptor, callback, mInjector, mConstants); + } else { + updateInstaller = new NonAbUpdateInstaller( + mContext, updateFileDescriptor, callback, mInjector, mConstants); + } + updateInstaller.startInstallUpdate(); + } finally { + mInjector.binderRestoreCallingIdentity(id); + } + } + + + private boolean isDeviceAB() { + return "true".equalsIgnoreCase(android.os.SystemProperties + .get(AB_DEVICE_KEY, "")); + } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NonAbUpdateInstaller.java b/services/devicepolicy/java/com/android/server/devicepolicy/NonAbUpdateInstaller.java new file mode 100644 index 000000000000..5f1e92682ac1 --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/NonAbUpdateInstaller.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.devicepolicy; + +import android.app.admin.DevicePolicyManager; +import android.app.admin.StartInstallingUpdateCallback; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.os.RecoverySystem; +import android.util.Log; + +import java.io.IOException; + +/** + * Used for installing an update for non + * AB devices. + */ +class NonAbUpdateInstaller extends UpdateInstaller { + NonAbUpdateInstaller(Context context, + ParcelFileDescriptor updateFileDescriptor, + StartInstallingUpdateCallback callback, DevicePolicyManagerService.Injector injector, + DevicePolicyConstants constants) { + super(context, updateFileDescriptor, callback, injector, constants); + } + + @Override + public void installUpdateInThread() { + try { + RecoverySystem.installPackage(mContext, mCopiedUpdateFile); + notifyCallbackOnSuccess(); + } catch (IOException e) { + Log.w(TAG, "IO error while trying to install non AB update.", e); + notifyCallbackOnError( + DevicePolicyManager.InstallUpdateCallback.UPDATE_ERROR_UNKNOWN, + Log.getStackTraceString(e)); + } + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/UpdateInstaller.java b/services/devicepolicy/java/com/android/server/devicepolicy/UpdateInstaller.java new file mode 100644 index 000000000000..7910598d8429 --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/UpdateInstaller.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018 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.devicepolicy; + +import android.app.admin.DevicePolicyManager; +import android.app.admin.StartInstallingUpdateCallback; +import android.content.Context; +import android.os.BatteryManager; +import android.os.Environment; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +abstract class UpdateInstaller { + private StartInstallingUpdateCallback mCallback; + private ParcelFileDescriptor mUpdateFileDescriptor; + private DevicePolicyConstants mConstants; + protected Context mContext; + protected File mCopiedUpdateFile; + + static final String TAG = "UpdateInstaller"; + private DevicePolicyManagerService.Injector mInjector; + + protected UpdateInstaller(Context context, ParcelFileDescriptor updateFileDescriptor, + StartInstallingUpdateCallback callback, DevicePolicyManagerService.Injector injector, + DevicePolicyConstants constants) { + mContext = context; + mCallback = callback; + mUpdateFileDescriptor = updateFileDescriptor; + mInjector = injector; + mConstants = constants; + } + + public abstract void installUpdateInThread(); + + public void startInstallUpdate() { + if (!checkIfBatteryIsSufficient()) { + notifyCallbackOnError( + DevicePolicyManager.InstallUpdateCallback.UPDATE_ERROR_BATTERY_LOW, + "The battery level must be above " + + mConstants.BATTERY_THRESHOLD_NOT_CHARGING + " while not charging or" + + "above " + mConstants.BATTERY_THRESHOLD_CHARGING + " while charging"); + return; + } + Thread thread = new Thread(() -> { + mCopiedUpdateFile = copyUpdateFileToDataOtaPackageDir(); + if (mCopiedUpdateFile == null) { + notifyCallbackOnError( + DevicePolicyManager.InstallUpdateCallback.UPDATE_ERROR_UNKNOWN, + "Error while copying file."); + return; + } + installUpdateInThread(); + }); + thread.setPriority(Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + } + + private boolean checkIfBatteryIsSufficient() { + BatteryManager batteryManager = + (BatteryManager) mContext.getSystemService(Context.BATTERY_SERVICE); + if (batteryManager != null) { + int chargePercentage = batteryManager + .getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); + return batteryManager.isCharging() + ? chargePercentage >= mConstants.BATTERY_THRESHOLD_CHARGING + : chargePercentage >= mConstants.BATTERY_THRESHOLD_NOT_CHARGING; + } + return false; + } + + private File copyUpdateFileToDataOtaPackageDir() { + try { + File destination = createNewFileWithPermissions(); + copyToFile(destination); + return destination; + } catch (IOException e) { + Log.w(TAG, "Failed to copy update file to OTA directory", e); + notifyCallbackOnError( + DevicePolicyManager.InstallUpdateCallback.UPDATE_ERROR_UNKNOWN, + Log.getStackTraceString(e)); + return null; + } + } + + private File createNewFileWithPermissions() throws IOException { + File destination = File.createTempFile( + "update", ".zip", new File(Environment.getDataDirectory() + "/ota_package")); + FileUtils.setPermissions( + /* path= */ destination, + /* mode= */ FileUtils.S_IRWXU | FileUtils.S_IRGRP | FileUtils.S_IROTH, + /* uid= */ -1, /* gid= */ -1); + return destination; + } + + private void copyToFile(File destination) throws IOException { + try (OutputStream out = new FileOutputStream(destination); + InputStream in = new ParcelFileDescriptor.AutoCloseInputStream( + mUpdateFileDescriptor)) { + FileUtils.copy(in, out); + } + } + + void cleanupUpdateFile() { + if (mCopiedUpdateFile.exists()) { + mCopiedUpdateFile.delete(); + } + } + + protected void notifyCallbackOnError(int errorCode, String errorMessage) { + cleanupUpdateFile(); + try { + mCallback.onStartInstallingUpdateError(errorCode, errorMessage); + } catch (RemoteException e) { + Log.d(TAG, "Error while calling callback", e); + } + } + + protected void notifyCallbackOnSuccess() { + cleanupUpdateFile(); + mInjector.powerManagerReboot(PowerManager.REBOOT_REQUESTED_BY_DEVICE_OWNER); + } +}