1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.systemupdatersample; 18 19 import android.content.Context; 20 import android.os.Handler; 21 import android.os.UpdateEngine; 22 import android.os.UpdateEngineCallback; 23 import android.util.Log; 24 25 import com.example.android.systemupdatersample.services.PrepareUpdateService; 26 import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; 27 import com.example.android.systemupdatersample.util.UpdateEngineProperties; 28 import com.google.common.base.Preconditions; 29 import com.google.common.collect.ImmutableList; 30 import com.google.common.util.concurrent.AtomicDouble; 31 32 import java.util.ArrayList; 33 import java.util.Collections; 34 import java.util.List; 35 import java.util.Optional; 36 import java.util.concurrent.atomic.AtomicBoolean; 37 import java.util.concurrent.atomic.AtomicInteger; 38 import java.util.function.DoubleConsumer; 39 import java.util.function.IntConsumer; 40 41 import javax.annotation.concurrent.GuardedBy; 42 43 /** 44 * Manages the update flow. It has its own state (in memory), separate from 45 * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}. 46 */ 47 public class UpdateManager { 48 49 private static final String TAG = "UpdateManager"; 50 51 /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */ 52 static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 53 + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; 54 55 private final UpdateEngine mUpdateEngine; 56 57 private AtomicInteger mUpdateEngineStatus = 58 new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); 59 private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN); 60 private AtomicDouble mProgress = new AtomicDouble(0); 61 private UpdaterState mUpdaterState = new UpdaterState(UpdaterState.IDLE); 62 63 private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true); 64 65 /** Synchronize state with engine status only once when app binds to UpdateEngine. */ 66 private AtomicBoolean mStateSynchronized = new AtomicBoolean(false); 67 68 @GuardedBy("mLock") 69 private UpdateData mLastUpdateData = null; 70 71 @GuardedBy("mLock") 72 private IntConsumer mOnStateChangeCallback = null; 73 @GuardedBy("mLock") 74 private IntConsumer mOnEngineStatusUpdateCallback = null; 75 @GuardedBy("mLock") 76 private DoubleConsumer mOnProgressUpdateCallback = null; 77 @GuardedBy("mLock") 78 private IntConsumer mOnEngineCompleteCallback = null; 79 80 private final Object mLock = new Object(); 81 82 private final UpdateManager.UpdateEngineCallbackImpl 83 mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl(); 84 85 private final Handler mHandler; 86 87 /** 88 * @param updateEngine UpdateEngine instance. 89 * @param handler Handler for {@link PrepareUpdateService} intent service. 90 */ UpdateManager(UpdateEngine updateEngine, Handler handler)91 public UpdateManager(UpdateEngine updateEngine, Handler handler) { 92 this.mUpdateEngine = updateEngine; 93 this.mHandler = handler; 94 } 95 96 /** 97 * Binds to {@link UpdateEngine}. Invokes onStateChangeCallback if present. 98 */ bind()99 public void bind() { 100 getOnStateChangeCallback().ifPresent(callback -> callback.accept(mUpdaterState.get())); 101 102 mStateSynchronized.set(false); 103 this.mUpdateEngine.bind(mUpdateEngineCallback); 104 } 105 106 /** 107 * Unbinds from {@link UpdateEngine}. 108 */ unbind()109 public void unbind() { 110 this.mUpdateEngine.unbind(); 111 } 112 getUpdaterState()113 public int getUpdaterState() { 114 return mUpdaterState.get(); 115 } 116 117 /** 118 * Returns true if manual switching slot is required. Value depends on 119 * the update config {@code ab_config.force_switch_slot}. 120 */ isManualSwitchSlotRequired()121 public boolean isManualSwitchSlotRequired() { 122 return mManualSwitchSlotRequired.get(); 123 } 124 125 /** 126 * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one 127 * of the values from {@link UpdaterState}. 128 * 129 * @param onStateChangeCallback a callback with parameter {@code state}. 130 */ setOnStateChangeCallback(IntConsumer onStateChangeCallback)131 public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) { 132 synchronized (mLock) { 133 this.mOnStateChangeCallback = onStateChangeCallback; 134 } 135 } 136 getOnStateChangeCallback()137 private Optional<IntConsumer> getOnStateChangeCallback() { 138 synchronized (mLock) { 139 return mOnStateChangeCallback == null 140 ? Optional.empty() 141 : Optional.of(mOnStateChangeCallback); 142 } 143 } 144 145 /** 146 * Sets update engine status update callback. Value of {@code status} will 147 * be one of the values from {@link UpdateEngine.UpdateStatusConstants}. 148 * 149 * @param onStatusUpdateCallback a callback with parameter {@code status}. 150 */ setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback)151 public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) { 152 synchronized (mLock) { 153 this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback; 154 } 155 } 156 getOnEngineStatusUpdateCallback()157 private Optional<IntConsumer> getOnEngineStatusUpdateCallback() { 158 synchronized (mLock) { 159 return mOnEngineStatusUpdateCallback == null 160 ? Optional.empty() 161 : Optional.of(mOnEngineStatusUpdateCallback); 162 } 163 } 164 165 /** 166 * Sets update engine payload application complete callback. Value of {@code errorCode} will 167 * be one of the values from {@link UpdateEngine.ErrorCodeConstants}. 168 * 169 * @param onComplete a callback with parameter {@code errorCode}. 170 */ setOnEngineCompleteCallback(IntConsumer onComplete)171 public void setOnEngineCompleteCallback(IntConsumer onComplete) { 172 synchronized (mLock) { 173 this.mOnEngineCompleteCallback = onComplete; 174 } 175 } 176 getOnEngineCompleteCallback()177 private Optional<IntConsumer> getOnEngineCompleteCallback() { 178 synchronized (mLock) { 179 return mOnEngineCompleteCallback == null 180 ? Optional.empty() 181 : Optional.of(mOnEngineCompleteCallback); 182 } 183 } 184 185 /** 186 * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}. 187 * 188 * @param onProgressCallback a callback with parameter {@code progress}. 189 */ setOnProgressUpdateCallback(DoubleConsumer onProgressCallback)190 public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) { 191 synchronized (mLock) { 192 this.mOnProgressUpdateCallback = onProgressCallback; 193 } 194 } 195 getOnProgressUpdateCallback()196 private Optional<DoubleConsumer> getOnProgressUpdateCallback() { 197 synchronized (mLock) { 198 return mOnProgressUpdateCallback == null 199 ? Optional.empty() 200 : Optional.of(mOnProgressUpdateCallback); 201 } 202 } 203 204 /** 205 * Suspend running update. 206 */ suspend()207 public synchronized void suspend() throws UpdaterState.InvalidTransitionException { 208 Log.d(TAG, "suspend invoked"); 209 setUpdaterState(UpdaterState.PAUSED); 210 mUpdateEngine.cancel(); 211 } 212 213 /** 214 * Resume suspended update. 215 */ resume()216 public synchronized void resume() throws UpdaterState.InvalidTransitionException { 217 Log.d(TAG, "resume invoked"); 218 setUpdaterState(UpdaterState.RUNNING); 219 updateEngineReApplyPayload(); 220 } 221 222 /** 223 * Updates {@link this.mState} and if state is changed, 224 * it also notifies {@link this.mOnStateChangeCallback}. 225 */ setUpdaterState(int newUpdaterState)226 private void setUpdaterState(int newUpdaterState) 227 throws UpdaterState.InvalidTransitionException { 228 Log.d(TAG, "setUpdaterState invoked newState=" + newUpdaterState); 229 int previousState = mUpdaterState.get(); 230 mUpdaterState.set(newUpdaterState); 231 if (previousState != newUpdaterState) { 232 getOnStateChangeCallback().ifPresent(callback -> callback.accept(newUpdaterState)); 233 } 234 } 235 236 /** 237 * Same as {@link this.setUpdaterState}. Logs the error if new state 238 * cannot be set. 239 */ setUpdaterStateSilent(int newUpdaterState)240 private void setUpdaterStateSilent(int newUpdaterState) { 241 try { 242 setUpdaterState(newUpdaterState); 243 } catch (UpdaterState.InvalidTransitionException e) { 244 // Most likely UpdateEngine status and UpdaterSample state got de-synchronized. 245 // To make sample app simple, we don't handle it properly. 246 Log.e(TAG, "Failed to set updater state", e); 247 } 248 } 249 250 /** 251 * Creates new UpdaterState, assigns it to {@link this.mUpdaterState}, 252 * and notifies callbacks. 253 */ initializeUpdateState(int state)254 private void initializeUpdateState(int state) { 255 this.mUpdaterState = new UpdaterState(state); 256 getOnStateChangeCallback().ifPresent(callback -> callback.accept(state)); 257 } 258 259 /** 260 * Requests update engine to stop any ongoing update. If an update has been applied, 261 * leave it as is. 262 */ cancelRunningUpdate()263 public synchronized void cancelRunningUpdate() throws UpdaterState.InvalidTransitionException { 264 Log.d(TAG, "cancelRunningUpdate invoked"); 265 setUpdaterState(UpdaterState.IDLE); 266 mUpdateEngine.cancel(); 267 } 268 269 /** 270 * Resets update engine to IDLE state. If an update has been applied it reverts it. 271 */ resetUpdate()272 public synchronized void resetUpdate() throws UpdaterState.InvalidTransitionException { 273 Log.d(TAG, "resetUpdate invoked"); 274 setUpdaterState(UpdaterState.IDLE); 275 mUpdateEngine.resetStatus(); 276 } 277 278 /** 279 * Applies the given update. 280 * 281 * <p>UpdateEngine works asynchronously. This method doesn't wait until 282 * end of the update.</p> 283 */ applyUpdate(Context context, UpdateConfig config)284 public synchronized void applyUpdate(Context context, UpdateConfig config) 285 throws UpdaterState.InvalidTransitionException { 286 mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN); 287 setUpdaterState(UpdaterState.RUNNING); 288 289 synchronized (mLock) { 290 // Cleaning up previous update data. 291 mLastUpdateData = null; 292 } 293 294 if (!config.getAbConfig().getForceSwitchSlot()) { 295 mManualSwitchSlotRequired.set(true); 296 } else { 297 mManualSwitchSlotRequired.set(false); 298 } 299 300 Log.d(TAG, "Starting PrepareUpdateService"); 301 PrepareUpdateService.startService(context, config, mHandler, (code, payloadSpec) -> { 302 if (code != PrepareUpdateService.RESULT_CODE_SUCCESS) { 303 Log.e(TAG, "PrepareUpdateService failed, result code is " + code); 304 setUpdaterStateSilent(UpdaterState.ERROR); 305 return; 306 } 307 updateEngineApplyPayload(UpdateData.builder() 308 .setExtraProperties(prepareExtraProperties(config)) 309 .setPayload(payloadSpec) 310 .build()); 311 }); 312 } 313 prepareExtraProperties(UpdateConfig config)314 private List<String> prepareExtraProperties(UpdateConfig config) { 315 List<String> extraProperties = new ArrayList<>(); 316 317 if (!config.getAbConfig().getForceSwitchSlot()) { 318 // Disable switch slot on reboot, which is enabled by default. 319 // User will enable it manually by clicking "Switch Slot" button on the screen. 320 extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT); 321 } 322 if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_STREAMING) { 323 extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); 324 config.getAbConfig() 325 .getAuthorization() 326 .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); 327 } 328 return extraProperties; 329 } 330 331 /** 332 * Applies given payload. 333 * 334 * <p>UpdateEngine works asynchronously. This method doesn't wait until 335 * end of the update.</p> 336 * 337 * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid 338 * payload properties (which come from OTA packages), or failing to set up the network 339 * with the given id.</p> 340 */ updateEngineApplyPayload(UpdateData update)341 private void updateEngineApplyPayload(UpdateData update) { 342 Log.d(TAG, "updateEngineApplyPayload invoked with url " + update.mPayload.getUrl()); 343 344 synchronized (mLock) { 345 mLastUpdateData = update; 346 } 347 348 ArrayList<String> properties = new ArrayList<>(update.getPayload().getProperties()); 349 properties.addAll(update.getExtraProperties()); 350 351 try { 352 mUpdateEngine.applyPayload( 353 update.getPayload().getUrl(), 354 update.getPayload().getOffset(), 355 update.getPayload().getSize(), 356 properties.toArray(new String[0])); 357 } catch (Exception e) { 358 Log.e(TAG, "UpdateEngine failed to apply the update", e); 359 setUpdaterStateSilent(UpdaterState.ERROR); 360 } 361 } 362 363 /** 364 * Re-applies {@link this.mLastUpdateData} to update_engine. 365 */ updateEngineReApplyPayload()366 private void updateEngineReApplyPayload() { 367 Log.d(TAG, "updateEngineReApplyPayload invoked"); 368 UpdateData lastUpdate; 369 synchronized (mLock) { 370 // mLastPayloadSpec might be empty in some cases. 371 // But to make this sample app simple, we will not handle it. 372 Preconditions.checkArgument( 373 mLastUpdateData != null, 374 "mLastUpdateData must be present."); 375 lastUpdate = mLastUpdateData; 376 } 377 updateEngineApplyPayload(lastUpdate); 378 } 379 380 /** 381 * Sets the new slot that has the updated partitions as the active slot, 382 * which device will boot into next time. 383 * This method is only supposed to be called after the payload is applied. 384 * 385 * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size 386 * and payload metadata headers doesn't trigger new update. It can be used to just switch 387 * active A/B slot. 388 * 389 * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will 390 * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}. 391 */ setSwitchSlotOnReboot()392 public synchronized void setSwitchSlotOnReboot() { 393 Log.d(TAG, "setSwitchSlotOnReboot invoked"); 394 395 // When mManualSwitchSlotRequired set false, next time 396 // onApplicationPayloadComplete is called, 397 // it will set updater state to REBOOT_REQUIRED. 398 mManualSwitchSlotRequired.set(false); 399 400 UpdateData.Builder builder; 401 synchronized (mLock) { 402 // To make sample app simple, we don't handle it. 403 Preconditions.checkArgument( 404 mLastUpdateData != null, 405 "mLastUpdateData must be present."); 406 builder = mLastUpdateData.toBuilder(); 407 } 408 // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks. 409 builder.setExtraProperties( 410 Collections.singletonList(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL)); 411 // UpdateEngine sets property SWITCH_SLOT_ON_REBOOT=1 by default. 412 // HTTP headers are not required, UpdateEngine is not expected to stream payload. 413 updateEngineApplyPayload(builder.build()); 414 } 415 416 /** 417 * Synchronize UpdaterState with UpdateEngine status. 418 * Apply necessary UpdateEngine operation if status are out of sync. 419 * 420 * It's expected to be called once when sample app binds itself to UpdateEngine. 421 */ synchronizeUpdaterStateWithUpdateEngineStatus()422 private void synchronizeUpdaterStateWithUpdateEngineStatus() { 423 Log.d(TAG, "synchronizeUpdaterStateWithUpdateEngineStatus is invoked."); 424 425 int state = mUpdaterState.get(); 426 int engineStatus = mUpdateEngineStatus.get(); 427 428 if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) { 429 // If update has been installed before running the sample app, 430 // set state to REBOOT_REQUIRED. 431 initializeUpdateState(UpdaterState.REBOOT_REQUIRED); 432 return; 433 } 434 435 switch (state) { 436 case UpdaterState.IDLE: 437 case UpdaterState.ERROR: 438 case UpdaterState.PAUSED: 439 case UpdaterState.SLOT_SWITCH_REQUIRED: 440 // It might happen when update is started not from the sample app. 441 // To make the sample app simple, we won't handle this case. 442 Preconditions.checkState( 443 engineStatus == UpdateEngine.UpdateStatusConstants.IDLE, 444 "When mUpdaterState is %s, mUpdateEngineStatus " 445 + "must be 0/IDLE, but it is %s", 446 state, 447 engineStatus); 448 break; 449 case UpdaterState.RUNNING: 450 if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT 451 || engineStatus == UpdateEngine.UpdateStatusConstants.IDLE) { 452 Log.i(TAG, "ensureUpdateEngineStatusIsRunning - re-applying last payload"); 453 // Re-apply latest update. It makes update_engine to invoke 454 // onPayloadApplicationComplete callback. The callback notifies 455 // if update was successful or not. 456 updateEngineReApplyPayload(); 457 } 458 break; 459 case UpdaterState.REBOOT_REQUIRED: 460 // This might happen when update is installed by other means, 461 // and sample app is not aware of it. 462 // To make the sample app simple, we won't handle this case. 463 Preconditions.checkState( 464 engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT, 465 "When mUpdaterState is %s, mUpdateEngineStatus " 466 + "must be 6/UPDATED_NEED_REBOOT, but it is %s", 467 state, 468 engineStatus); 469 break; 470 default: 471 throw new IllegalStateException("This block should not be reached."); 472 } 473 } 474 475 /** 476 * Invoked by update_engine whenever update status or progress changes. 477 * It's also guaranteed to be invoked when app binds to the update_engine, except 478 * when update_engine fails to initialize (as defined in 479 * system/update_engine/binder_service_android.cc in 480 * function BinderUpdateEngineAndroidService::bind). 481 * 482 * @param status one of {@link UpdateEngine.UpdateStatusConstants}. 483 * @param progress a number from 0.0 to 1.0. 484 */ onStatusUpdate(int status, float progress)485 private void onStatusUpdate(int status, float progress) { 486 Log.d(TAG, String.format( 487 "onStatusUpdate invoked, status=%s, progress=%.2f", 488 status, 489 progress)); 490 491 int previousStatus = mUpdateEngineStatus.get(); 492 mUpdateEngineStatus.set(status); 493 mProgress.set(progress); 494 495 if (!mStateSynchronized.getAndSet(true)) { 496 // We synchronize state with engine status once 497 // only when sample app is bound to UpdateEngine. 498 synchronizeUpdaterStateWithUpdateEngineStatus(); 499 } 500 501 getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(mProgress.get())); 502 503 if (previousStatus != status) { 504 getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status)); 505 } 506 } 507 onPayloadApplicationComplete(int errorCode)508 private void onPayloadApplicationComplete(int errorCode) { 509 Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode); 510 mEngineErrorCode.set(errorCode); 511 if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS 512 || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) { 513 setUpdaterStateSilent(isManualSwitchSlotRequired() 514 ? UpdaterState.SLOT_SWITCH_REQUIRED 515 : UpdaterState.REBOOT_REQUIRED); 516 } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) { 517 setUpdaterStateSilent(UpdaterState.ERROR); 518 } 519 520 getOnEngineCompleteCallback() 521 .ifPresent(callback -> callback.accept(errorCode)); 522 } 523 524 /** 525 * Helper class to delegate {@code update_engine} callback invocations to UpdateManager. 526 */ 527 class UpdateEngineCallbackImpl extends UpdateEngineCallback { 528 @Override onStatusUpdate(int status, float percent)529 public void onStatusUpdate(int status, float percent) { 530 UpdateManager.this.onStatusUpdate(status, percent); 531 } 532 533 @Override onPayloadApplicationComplete(int errorCode)534 public void onPayloadApplicationComplete(int errorCode) { 535 UpdateManager.this.onPayloadApplicationComplete(errorCode); 536 } 537 } 538 539 /** 540 * Contains update data - PayloadSpec and extra properties list. 541 * 542 * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}. 543 * {@code mExtraProperties} is a list of additional properties to pass to 544 * {@link UpdateEngine#applyPayload}.</p> 545 */ 546 private static class UpdateData { 547 private final PayloadSpec mPayload; 548 private final ImmutableList<String> mExtraProperties; 549 builder()550 public static Builder builder() { 551 return new Builder(); 552 } 553 UpdateData(Builder builder)554 UpdateData(Builder builder) { 555 this.mPayload = builder.mPayload; 556 this.mExtraProperties = ImmutableList.copyOf(builder.mExtraProperties); 557 } 558 getPayload()559 public PayloadSpec getPayload() { 560 return mPayload; 561 } 562 getExtraProperties()563 public ImmutableList<String> getExtraProperties() { 564 return mExtraProperties; 565 } 566 toBuilder()567 public Builder toBuilder() { 568 return builder() 569 .setPayload(mPayload) 570 .setExtraProperties(mExtraProperties); 571 } 572 573 static class Builder { 574 private PayloadSpec mPayload; 575 private List<String> mExtraProperties; 576 setPayload(PayloadSpec payload)577 public Builder setPayload(PayloadSpec payload) { 578 this.mPayload = payload; 579 return this; 580 } 581 setExtraProperties(List<String> extraProperties)582 public Builder setExtraProperties(List<String> extraProperties) { 583 this.mExtraProperties = new ArrayList<>(extraProperties); 584 return this; 585 } 586 addExtraProperty(String property)587 public Builder addExtraProperty(String property) { 588 if (this.mExtraProperties == null) { 589 this.mExtraProperties = new ArrayList<>(); 590 } 591 this.mExtraProperties.add(property); 592 return this; 593 } 594 build()595 public UpdateData build() { 596 return new UpdateData(this); 597 } 598 } 599 } 600 601 } 602