blob: c4c8c9c27329f3df1401ddbf5b531463b9b29cf8 [file] [log] [blame]
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -07001/*
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
17package com.example.android.systemupdatersample;
18
19import android.content.Context;
20import android.os.UpdateEngine;
21import android.os.UpdateEngineCallback;
22import android.util.Log;
23
24import com.example.android.systemupdatersample.services.PrepareStreamingService;
25import com.example.android.systemupdatersample.util.PayloadSpecs;
26import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
27import com.example.android.systemupdatersample.util.UpdateEngineProperties;
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070028import com.google.common.base.Preconditions;
29import com.google.common.collect.ImmutableList;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070030import com.google.common.util.concurrent.AtomicDouble;
31
32import java.io.IOException;
33import java.util.ArrayList;
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070034import java.util.Collections;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070035import java.util.List;
36import java.util.Optional;
37import java.util.concurrent.atomic.AtomicBoolean;
38import java.util.concurrent.atomic.AtomicInteger;
39import java.util.function.DoubleConsumer;
40import java.util.function.IntConsumer;
41
42/**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070043 * Manages the update flow. It has its own state (in memory), separate from
44 * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}.
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070045 */
46public class UpdateManager {
47
48 private static final String TAG = "UpdateManager";
49
50 /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */
51 private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
52 + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
53
54 private final UpdateEngine mUpdateEngine;
55 private final PayloadSpecs mPayloadSpecs;
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);
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -070061 private UpdaterState mUpdaterState = new UpdaterState(UpdaterState.IDLE);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070062
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070063 private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
64
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070065 private UpdateData mLastUpdateData = null;
66
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070067 private IntConsumer mOnStateChangeCallback = null;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070068 private IntConsumer mOnEngineStatusUpdateCallback = null;
69 private DoubleConsumer mOnProgressUpdateCallback = null;
70 private IntConsumer mOnEngineCompleteCallback = null;
71
72 private final Object mLock = new Object();
73
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070074 private final UpdateManager.UpdateEngineCallbackImpl
75 mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl();
76
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070077 public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) {
78 this.mUpdateEngine = updateEngine;
79 this.mPayloadSpecs = payloadSpecs;
80 }
81
82 /**
83 * Binds to {@link UpdateEngine}.
84 */
85 public void bind() {
86 this.mUpdateEngine.bind(mUpdateEngineCallback);
87 }
88
89 /**
90 * Unbinds from {@link UpdateEngine}.
91 */
92 public void unbind() {
93 this.mUpdateEngine.unbind();
94 }
95
96 /**
97 * @return a number from {@code 0.0} to {@code 1.0}.
98 */
99 public float getProgress() {
100 return (float) this.mProgress.get();
101 }
102
103 /**
104 * Returns true if manual switching slot is required. Value depends on
105 * the update config {@code ab_config.force_switch_slot}.
106 */
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700107 public boolean isManualSwitchSlotRequired() {
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700108 return mManualSwitchSlotRequired.get();
109 }
110
111 /**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700112 * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700113 * of the values from {@link UpdaterState}.
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700114 *
115 * @param onStateChangeCallback a callback with parameter {@code state}.
116 */
117 public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) {
118 synchronized (mLock) {
119 this.mOnStateChangeCallback = onStateChangeCallback;
120 }
121 }
122
123 private Optional<IntConsumer> getOnStateChangeCallback() {
124 synchronized (mLock) {
125 return mOnStateChangeCallback == null
126 ? Optional.empty()
127 : Optional.of(mOnStateChangeCallback);
128 }
129 }
130
131 /**
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700132 * Sets update engine status update callback. Value of {@code status} will
133 * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
134 *
135 * @param onStatusUpdateCallback a callback with parameter {@code status}.
136 */
137 public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) {
138 synchronized (mLock) {
139 this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback;
140 }
141 }
142
143 private Optional<IntConsumer> getOnEngineStatusUpdateCallback() {
144 synchronized (mLock) {
145 return mOnEngineStatusUpdateCallback == null
146 ? Optional.empty()
147 : Optional.of(mOnEngineStatusUpdateCallback);
148 }
149 }
150
151 /**
152 * Sets update engine payload application complete callback. Value of {@code errorCode} will
153 * be one of the values from {@link UpdateEngine.ErrorCodeConstants}.
154 *
155 * @param onComplete a callback with parameter {@code errorCode}.
156 */
157 public void setOnEngineCompleteCallback(IntConsumer onComplete) {
158 synchronized (mLock) {
159 this.mOnEngineCompleteCallback = onComplete;
160 }
161 }
162
163 private Optional<IntConsumer> getOnEngineCompleteCallback() {
164 synchronized (mLock) {
165 return mOnEngineCompleteCallback == null
166 ? Optional.empty()
167 : Optional.of(mOnEngineCompleteCallback);
168 }
169 }
170
171 /**
172 * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}.
173 *
174 * @param onProgressCallback a callback with parameter {@code progress}.
175 */
176 public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) {
177 synchronized (mLock) {
178 this.mOnProgressUpdateCallback = onProgressCallback;
179 }
180 }
181
182 private Optional<DoubleConsumer> getOnProgressUpdateCallback() {
183 synchronized (mLock) {
184 return mOnProgressUpdateCallback == null
185 ? Optional.empty()
186 : Optional.of(mOnProgressUpdateCallback);
187 }
188 }
189
190 /**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700191 * Updates {@link this.mState} and if state is changed,
192 * it also notifies {@link this.mOnStateChangeCallback}.
193 */
194 private void setUpdaterState(int updaterState) {
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700195 int previousState = mUpdaterState.get();
196 try {
197 mUpdaterState.set(updaterState);
198 } catch (UpdaterState.InvalidTransitionException e) {
199 // Note: invalid state transitions should be handled properly,
200 // but to make sample app simple, we just throw runtime exception.
201 throw new RuntimeException("Can't set state " + updaterState, e);
202 }
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700203 if (previousState != updaterState) {
204 getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState));
205 }
206 }
207
208 /**
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700209 * Requests update engine to stop any ongoing update. If an update has been applied,
210 * leave it as is.
211 *
212 * <p>Sometimes it's possible that the
213 * update engine would throw an error when the method is called, and the only way to
214 * handle it is to catch the exception.</p>
215 */
216 public void cancelRunningUpdate() {
217 try {
218 mUpdateEngine.cancel();
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700219 setUpdaterState(UpdaterState.IDLE);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700220 } catch (Exception e) {
221 Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
222 }
223 }
224
225 /**
226 * Resets update engine to IDLE state. If an update has been applied it reverts it.
227 *
228 * <p>Sometimes it's possible that the
229 * update engine would throw an error when the method is called, and the only way to
230 * handle it is to catch the exception.</p>
231 */
232 public void resetUpdate() {
233 try {
234 mUpdateEngine.resetStatus();
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700235 setUpdaterState(UpdaterState.IDLE);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700236 } catch (Exception e) {
237 Log.w(TAG, "UpdateEngine failed to reset the update", e);
238 }
239 }
240
241 /**
242 * Applies the given update.
243 *
244 * <p>UpdateEngine works asynchronously. This method doesn't wait until
245 * end of the update.</p>
246 */
247 public void applyUpdate(Context context, UpdateConfig config) {
248 mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700249 setUpdaterState(UpdaterState.RUNNING);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700250
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700251 synchronized (mLock) {
252 // Cleaning up previous update data.
253 mLastUpdateData = null;
254 }
255
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700256 if (!config.getAbConfig().getForceSwitchSlot()) {
257 mManualSwitchSlotRequired.set(true);
258 } else {
259 mManualSwitchSlotRequired.set(false);
260 }
261
262 if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
263 applyAbNonStreamingUpdate(config);
264 } else {
265 applyAbStreamingUpdate(context, config);
266 }
267 }
268
269 private void applyAbNonStreamingUpdate(UpdateConfig config) {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700270 UpdateData.Builder builder = UpdateData.builder()
271 .setExtraProperties(prepareExtraProperties(config));
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700272
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700273 try {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700274 builder.setPayload(mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()));
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700275 } catch (IOException e) {
276 Log.e(TAG, "Error creating payload spec", e);
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700277 setUpdaterState(UpdaterState.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700278 return;
279 }
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700280 updateEngineApplyPayload(builder.build());
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700281 }
282
283 private void applyAbStreamingUpdate(Context context, UpdateConfig config) {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700284 UpdateData.Builder builder = UpdateData.builder()
285 .setExtraProperties(prepareExtraProperties(config));
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700286
287 Log.d(TAG, "Starting PrepareStreamingService");
288 PrepareStreamingService.startService(context, config, (code, payloadSpec) -> {
289 if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700290 builder.setPayload(payloadSpec);
291 builder.addExtraProperty("USER_AGENT=" + HTTP_USER_AGENT);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700292 config.getStreamingMetadata()
293 .getAuthorization()
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700294 .ifPresent(s -> builder.addExtraProperty("AUTHORIZATION=" + s));
295 updateEngineApplyPayload(builder.build());
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700296 } else {
297 Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700298 setUpdaterState(UpdaterState.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700299 }
300 });
301 }
302
303 private List<String> prepareExtraProperties(UpdateConfig config) {
304 List<String> extraProperties = new ArrayList<>();
305
306 if (!config.getAbConfig().getForceSwitchSlot()) {
307 // Disable switch slot on reboot, which is enabled by default.
308 // User will enable it manually by clicking "Switch Slot" button on the screen.
309 extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT);
310 }
311 return extraProperties;
312 }
313
314 /**
315 * Applies given payload.
316 *
317 * <p>UpdateEngine works asynchronously. This method doesn't wait until
318 * end of the update.</p>
319 *
320 * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid
321 * payload properties (which come from OTA packages), or failing to set up the network
322 * with the given id.</p>
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700323 */
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700324 private void updateEngineApplyPayload(UpdateData update) {
325 synchronized (mLock) {
326 mLastUpdateData = update;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700327 }
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700328
329 ArrayList<String> properties = new ArrayList<>(update.getPayload().getProperties());
330 properties.addAll(update.getExtraProperties());
331
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700332 try {
333 mUpdateEngine.applyPayload(
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700334 update.getPayload().getUrl(),
335 update.getPayload().getOffset(),
336 update.getPayload().getSize(),
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700337 properties.toArray(new String[0]));
338 } catch (Exception e) {
339 Log.e(TAG, "UpdateEngine failed to apply the update", e);
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700340 setUpdaterState(UpdaterState.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700341 }
342 }
343
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700344 private void updateEngineReApplyPayload() {
345 UpdateData lastUpdate;
346 synchronized (mLock) {
347 // mLastPayloadSpec might be empty in some cases.
348 // But to make this sample app simple, we will not handle it.
349 Preconditions.checkArgument(
350 mLastUpdateData != null,
351 "mLastUpdateData must be present.");
352 lastUpdate = mLastUpdateData;
353 }
354 updateEngineApplyPayload(lastUpdate);
355 }
356
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700357 /**
358 * Sets the new slot that has the updated partitions as the active slot,
359 * which device will boot into next time.
360 * This method is only supposed to be called after the payload is applied.
361 *
362 * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size
363 * and payload metadata headers doesn't trigger new update. It can be used to just switch
364 * active A/B slot.
365 *
366 * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
367 * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
368 */
369 public void setSwitchSlotOnReboot() {
370 Log.d(TAG, "setSwitchSlotOnReboot invoked");
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700371 UpdateData.Builder builder;
372 synchronized (mLock) {
373 // To make sample app simple, we don't handle it.
374 Preconditions.checkArgument(
375 mLastUpdateData != null,
376 "mLastUpdateData must be present.");
377 builder = mLastUpdateData.toBuilder();
378 }
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700379 // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks.
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700380 builder.setExtraProperties(
381 Collections.singletonList(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL));
382 // UpdateEngine sets property SWITCH_SLOT_ON_REBOOT=1 by default.
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700383 // HTTP headers are not required, UpdateEngine is not expected to stream payload.
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700384 updateEngineApplyPayload(builder.build());
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700385 }
386
387 private void onStatusUpdate(int status, float progress) {
388 int previousStatus = mUpdateEngineStatus.get();
389 mUpdateEngineStatus.set(status);
390 mProgress.set(progress);
391
392 getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress));
393
394 if (previousStatus != status) {
395 getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status));
396 }
397 }
398
399 private void onPayloadApplicationComplete(int errorCode) {
400 Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode);
401 mEngineErrorCode.set(errorCode);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700402 if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
403 || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700404 setUpdaterState(isManualSwitchSlotRequired()
405 ? UpdaterState.SLOT_SWITCH_REQUIRED
406 : UpdaterState.REBOOT_REQUIRED);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700407 } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
Zhomart Mukhamejanov674aa6c2018-05-25 17:00:11 -0700408 setUpdaterState(UpdaterState.ERROR);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700409 }
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700410
411 getOnEngineCompleteCallback()
412 .ifPresent(callback -> callback.accept(errorCode));
413 }
414
415 /**
416 * Helper class to delegate {@code update_engine} callbacks to UpdateManager
417 */
418 class UpdateEngineCallbackImpl extends UpdateEngineCallback {
419 @Override
420 public void onStatusUpdate(int status, float percent) {
421 UpdateManager.this.onStatusUpdate(status, percent);
422 }
423
424 @Override
425 public void onPayloadApplicationComplete(int errorCode) {
426 UpdateManager.this.onPayloadApplicationComplete(errorCode);
427 }
428 }
429
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700430 /**
431 *
432 * Contains update data - PayloadSpec and extra properties list.
433 *
434 * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}.
435 * {@code mExtraProperties} is a list of additional properties to pass to
436 * {@link UpdateEngine#applyPayload}.</p>
437 */
438 private static class UpdateData {
439 private final PayloadSpec mPayload;
440 private final ImmutableList<String> mExtraProperties;
441
442 public static Builder builder() {
443 return new Builder();
444 }
445
446 UpdateData(Builder builder) {
447 this.mPayload = builder.mPayload;
448 this.mExtraProperties = ImmutableList.copyOf(builder.mExtraProperties);
449 }
450
451 public PayloadSpec getPayload() {
452 return mPayload;
453 }
454
455 public ImmutableList<String> getExtraProperties() {
456 return mExtraProperties;
457 }
458
459 public Builder toBuilder() {
460 return builder()
461 .setPayload(mPayload)
462 .setExtraProperties(mExtraProperties);
463 }
464
465 static class Builder {
466 private PayloadSpec mPayload;
467 private List<String> mExtraProperties;
468
469 public Builder setPayload(PayloadSpec payload) {
470 this.mPayload = payload;
471 return this;
472 }
473
474 public Builder setExtraProperties(List<String> extraProperties) {
475 this.mExtraProperties = new ArrayList<>(extraProperties);
476 return this;
477 }
478
479 public Builder addExtraProperty(String property) {
480 if (this.mExtraProperties == null) {
481 this.mExtraProperties = new ArrayList<>();
482 }
483 this.mExtraProperties.add(property);
484 return this;
485 }
486
487 public UpdateData build() {
488 return new UpdateData(this);
489 }
490 }
491 }
492
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700493}