Merge "Managed System Updates API"
diff --git a/Android.bp b/Android.bp
index 7e038ce8..f40aab1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -92,6 +92,7 @@
"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 e84bc8d..abd3c31 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6598,6 +6598,7 @@
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 @@
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 1661963..75a2cc4 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -441,10 +441,10 @@
}
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 24ee7f7..00c1863 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -53,6 +53,7 @@
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.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;
@@ -1930,6 +1932,48 @@
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.
*
* @param admin The administrator component to check for.
@@ -6796,7 +6840,6 @@
@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
@@ -9827,6 +9870,62 @@
}
/**
+ * 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.
*
* @param admin which {@link DeviceAdminReceiver} this request is associated with.
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 918c127..60f79d6 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -20,6 +20,7 @@
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 @@
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 0000000..df04707
--- /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 0000000..05912a5
--- /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 <a href="https://source.android.com/devices/tech/ota/ab">AB
+ * devices.</a>
+ * <p>This logic is specific to GOTA and should be modified by OEMs using a different AB update
+ * system.</p>
+ */
+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<String> mProperties;
+ private Enumeration<? extends ZipEntry> mEntries;
+ private ZipFile mPackedUpdateFile;
+ private static final Map<Integer, Integer> errorCodesMap = buildErrorCodesMap();
+ private static final Map<Integer, String> errorStringsMap = buildErrorStringsMap();
+ public static final String UNKNOWN_ERROR = "Unknown error with error code = ";
+ private boolean mUpdateInstalled;
+
+ private static Map<Integer, Integer> buildErrorCodesMap() {
+ Map<Integer, Integer> 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<Integer, String> buildErrorStringsMap() {
+ Map<Integer, String> 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 5926bdd..6462d16 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 @@
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 @@
@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 71fea02..fd59b43 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 @@
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 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 @@
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 @@
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 bbbc40c..7751b4a 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.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 @@
private static final Set<String> GLOBAL_SETTINGS_DEPRECATED;
private static final Set<String> SYSTEM_SETTINGS_WHITELIST;
private static final Set<Integer> 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 @@
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 0000000..5f1e926
--- /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 <a href="https://source.android.com/devices/tech/ota/nonab">non
+ * AB</a> 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 0000000..7910598
--- /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);
+ }
+}