blob: bf673c2ebef0450b8b1000f059770cbfa32fe714 [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 Mukhamejanov8f4059d2018-05-18 10:15:31 -070028import com.example.android.systemupdatersample.util.UpdaterStates;
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070029import com.google.common.base.Preconditions;
30import com.google.common.collect.ImmutableList;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070031import com.google.common.util.concurrent.AtomicDouble;
32
33import java.io.IOException;
34import java.util.ArrayList;
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070035import java.util.Collections;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070036import java.util.List;
37import java.util.Optional;
38import java.util.concurrent.atomic.AtomicBoolean;
39import java.util.concurrent.atomic.AtomicInteger;
40import java.util.function.DoubleConsumer;
41import java.util.function.IntConsumer;
42
43/**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070044 * Manages the update flow. It has its own state (in memory), separate from
45 * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}.
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070046 */
47public 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 private 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 private final PayloadSpecs mPayloadSpecs;
57
58 private AtomicInteger mUpdateEngineStatus =
59 new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
60 private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN);
61 private AtomicDouble mProgress = new AtomicDouble(0);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070062 private AtomicInteger mState = new AtomicInteger(UpdaterStates.IDLE);
63
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070064 private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
65
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070066 private UpdateData mLastUpdateData = null;
67
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070068 private IntConsumer mOnStateChangeCallback = null;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070069 private IntConsumer mOnEngineStatusUpdateCallback = null;
70 private DoubleConsumer mOnProgressUpdateCallback = null;
71 private IntConsumer mOnEngineCompleteCallback = null;
72
73 private final Object mLock = new Object();
74
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -070075 private final UpdateManager.UpdateEngineCallbackImpl
76 mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl();
77
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070078 public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) {
79 this.mUpdateEngine = updateEngine;
80 this.mPayloadSpecs = payloadSpecs;
81 }
82
83 /**
84 * Binds to {@link UpdateEngine}.
85 */
86 public void bind() {
87 this.mUpdateEngine.bind(mUpdateEngineCallback);
88 }
89
90 /**
91 * Unbinds from {@link UpdateEngine}.
92 */
93 public void unbind() {
94 this.mUpdateEngine.unbind();
95 }
96
97 /**
98 * @return a number from {@code 0.0} to {@code 1.0}.
99 */
100 public float getProgress() {
101 return (float) this.mProgress.get();
102 }
103
104 /**
105 * Returns true if manual switching slot is required. Value depends on
106 * the update config {@code ab_config.force_switch_slot}.
107 */
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700108 public boolean isManualSwitchSlotRequired() {
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700109 return mManualSwitchSlotRequired.get();
110 }
111
112 /**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700113 * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one
114 * of the values from {@link UpdaterStates}.
115 *
116 * @param onStateChangeCallback a callback with parameter {@code state}.
117 */
118 public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) {
119 synchronized (mLock) {
120 this.mOnStateChangeCallback = onStateChangeCallback;
121 }
122 }
123
124 private Optional<IntConsumer> getOnStateChangeCallback() {
125 synchronized (mLock) {
126 return mOnStateChangeCallback == null
127 ? Optional.empty()
128 : Optional.of(mOnStateChangeCallback);
129 }
130 }
131
132 /**
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700133 * Sets update engine status update callback. Value of {@code status} will
134 * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
135 *
136 * @param onStatusUpdateCallback a callback with parameter {@code status}.
137 */
138 public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) {
139 synchronized (mLock) {
140 this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback;
141 }
142 }
143
144 private Optional<IntConsumer> getOnEngineStatusUpdateCallback() {
145 synchronized (mLock) {
146 return mOnEngineStatusUpdateCallback == null
147 ? Optional.empty()
148 : Optional.of(mOnEngineStatusUpdateCallback);
149 }
150 }
151
152 /**
153 * Sets update engine payload application complete callback. Value of {@code errorCode} will
154 * be one of the values from {@link UpdateEngine.ErrorCodeConstants}.
155 *
156 * @param onComplete a callback with parameter {@code errorCode}.
157 */
158 public void setOnEngineCompleteCallback(IntConsumer onComplete) {
159 synchronized (mLock) {
160 this.mOnEngineCompleteCallback = onComplete;
161 }
162 }
163
164 private Optional<IntConsumer> getOnEngineCompleteCallback() {
165 synchronized (mLock) {
166 return mOnEngineCompleteCallback == null
167 ? Optional.empty()
168 : Optional.of(mOnEngineCompleteCallback);
169 }
170 }
171
172 /**
173 * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}.
174 *
175 * @param onProgressCallback a callback with parameter {@code progress}.
176 */
177 public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) {
178 synchronized (mLock) {
179 this.mOnProgressUpdateCallback = onProgressCallback;
180 }
181 }
182
183 private Optional<DoubleConsumer> getOnProgressUpdateCallback() {
184 synchronized (mLock) {
185 return mOnProgressUpdateCallback == null
186 ? Optional.empty()
187 : Optional.of(mOnProgressUpdateCallback);
188 }
189 }
190
191 /**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700192 * Updates {@link this.mState} and if state is changed,
193 * it also notifies {@link this.mOnStateChangeCallback}.
194 */
195 private void setUpdaterState(int updaterState) {
196 int previousState = mState.get();
197 mState.set(updaterState);
198 if (previousState != updaterState) {
199 getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState));
200 }
201 }
202
203 /**
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700204 * Requests update engine to stop any ongoing update. If an update has been applied,
205 * leave it as is.
206 *
207 * <p>Sometimes it's possible that the
208 * update engine would throw an error when the method is called, and the only way to
209 * handle it is to catch the exception.</p>
210 */
211 public void cancelRunningUpdate() {
212 try {
213 mUpdateEngine.cancel();
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700214 setUpdaterState(UpdaterStates.IDLE);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700215 } catch (Exception e) {
216 Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
217 }
218 }
219
220 /**
221 * Resets update engine to IDLE state. If an update has been applied it reverts it.
222 *
223 * <p>Sometimes it's possible that the
224 * update engine would throw an error when the method is called, and the only way to
225 * handle it is to catch the exception.</p>
226 */
227 public void resetUpdate() {
228 try {
229 mUpdateEngine.resetStatus();
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700230 setUpdaterState(UpdaterStates.IDLE);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700231 } catch (Exception e) {
232 Log.w(TAG, "UpdateEngine failed to reset the update", e);
233 }
234 }
235
236 /**
237 * Applies the given update.
238 *
239 * <p>UpdateEngine works asynchronously. This method doesn't wait until
240 * end of the update.</p>
241 */
242 public void applyUpdate(Context context, UpdateConfig config) {
243 mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700244 setUpdaterState(UpdaterStates.RUNNING);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700245
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700246 synchronized (mLock) {
247 // Cleaning up previous update data.
248 mLastUpdateData = null;
249 }
250
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700251 if (!config.getAbConfig().getForceSwitchSlot()) {
252 mManualSwitchSlotRequired.set(true);
253 } else {
254 mManualSwitchSlotRequired.set(false);
255 }
256
257 if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
258 applyAbNonStreamingUpdate(config);
259 } else {
260 applyAbStreamingUpdate(context, config);
261 }
262 }
263
264 private void applyAbNonStreamingUpdate(UpdateConfig config) {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700265 UpdateData.Builder builder = UpdateData.builder()
266 .setExtraProperties(prepareExtraProperties(config));
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700267
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700268 try {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700269 builder.setPayload(mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()));
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700270 } catch (IOException e) {
271 Log.e(TAG, "Error creating payload spec", e);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700272 setUpdaterState(UpdaterStates.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700273 return;
274 }
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700275 updateEngineApplyPayload(builder.build());
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700276 }
277
278 private void applyAbStreamingUpdate(Context context, UpdateConfig config) {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700279 UpdateData.Builder builder = UpdateData.builder()
280 .setExtraProperties(prepareExtraProperties(config));
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700281
282 Log.d(TAG, "Starting PrepareStreamingService");
283 PrepareStreamingService.startService(context, config, (code, payloadSpec) -> {
284 if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700285 builder.setPayload(payloadSpec);
286 builder.addExtraProperty("USER_AGENT=" + HTTP_USER_AGENT);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700287 config.getStreamingMetadata()
288 .getAuthorization()
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700289 .ifPresent(s -> builder.addExtraProperty("AUTHORIZATION=" + s));
290 updateEngineApplyPayload(builder.build());
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700291 } else {
292 Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700293 setUpdaterState(UpdaterStates.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700294 }
295 });
296 }
297
298 private List<String> prepareExtraProperties(UpdateConfig config) {
299 List<String> extraProperties = new ArrayList<>();
300
301 if (!config.getAbConfig().getForceSwitchSlot()) {
302 // Disable switch slot on reboot, which is enabled by default.
303 // User will enable it manually by clicking "Switch Slot" button on the screen.
304 extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT);
305 }
306 return extraProperties;
307 }
308
309 /**
310 * Applies given payload.
311 *
312 * <p>UpdateEngine works asynchronously. This method doesn't wait until
313 * end of the update.</p>
314 *
315 * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid
316 * payload properties (which come from OTA packages), or failing to set up the network
317 * with the given id.</p>
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700318 */
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700319 private void updateEngineApplyPayload(UpdateData update) {
320 synchronized (mLock) {
321 mLastUpdateData = update;
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700322 }
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700323
324 ArrayList<String> properties = new ArrayList<>(update.getPayload().getProperties());
325 properties.addAll(update.getExtraProperties());
326
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700327 try {
328 mUpdateEngine.applyPayload(
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700329 update.getPayload().getUrl(),
330 update.getPayload().getOffset(),
331 update.getPayload().getSize(),
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700332 properties.toArray(new String[0]));
333 } catch (Exception e) {
334 Log.e(TAG, "UpdateEngine failed to apply the update", e);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700335 setUpdaterState(UpdaterStates.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700336 }
337 }
338
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700339 private void updateEngineReApplyPayload() {
340 UpdateData lastUpdate;
341 synchronized (mLock) {
342 // mLastPayloadSpec might be empty in some cases.
343 // But to make this sample app simple, we will not handle it.
344 Preconditions.checkArgument(
345 mLastUpdateData != null,
346 "mLastUpdateData must be present.");
347 lastUpdate = mLastUpdateData;
348 }
349 updateEngineApplyPayload(lastUpdate);
350 }
351
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700352 /**
353 * Sets the new slot that has the updated partitions as the active slot,
354 * which device will boot into next time.
355 * This method is only supposed to be called after the payload is applied.
356 *
357 * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size
358 * and payload metadata headers doesn't trigger new update. It can be used to just switch
359 * active A/B slot.
360 *
361 * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
362 * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
363 */
364 public void setSwitchSlotOnReboot() {
365 Log.d(TAG, "setSwitchSlotOnReboot invoked");
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700366 UpdateData.Builder builder;
367 synchronized (mLock) {
368 // To make sample app simple, we don't handle it.
369 Preconditions.checkArgument(
370 mLastUpdateData != null,
371 "mLastUpdateData must be present.");
372 builder = mLastUpdateData.toBuilder();
373 }
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700374 // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks.
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700375 builder.setExtraProperties(
376 Collections.singletonList(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL));
377 // UpdateEngine sets property SWITCH_SLOT_ON_REBOOT=1 by default.
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700378 // HTTP headers are not required, UpdateEngine is not expected to stream payload.
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700379 updateEngineApplyPayload(builder.build());
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700380 }
381
382 private void onStatusUpdate(int status, float progress) {
383 int previousStatus = mUpdateEngineStatus.get();
384 mUpdateEngineStatus.set(status);
385 mProgress.set(progress);
386
387 getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress));
388
389 if (previousStatus != status) {
390 getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status));
391 }
392 }
393
394 private void onPayloadApplicationComplete(int errorCode) {
395 Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode);
396 mEngineErrorCode.set(errorCode);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700397 if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
398 || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
399 setUpdaterState(UpdaterStates.FINISHED);
400 } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
401 setUpdaterState(UpdaterStates.ERROR);
402 }
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700403
404 getOnEngineCompleteCallback()
405 .ifPresent(callback -> callback.accept(errorCode));
406 }
407
408 /**
409 * Helper class to delegate {@code update_engine} callbacks to UpdateManager
410 */
411 class UpdateEngineCallbackImpl extends UpdateEngineCallback {
412 @Override
413 public void onStatusUpdate(int status, float percent) {
414 UpdateManager.this.onStatusUpdate(status, percent);
415 }
416
417 @Override
418 public void onPayloadApplicationComplete(int errorCode) {
419 UpdateManager.this.onPayloadApplicationComplete(errorCode);
420 }
421 }
422
Zhomart Mukhamejanovb34f7ea2018-05-25 17:00:11 -0700423 /**
424 *
425 * Contains update data - PayloadSpec and extra properties list.
426 *
427 * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}.
428 * {@code mExtraProperties} is a list of additional properties to pass to
429 * {@link UpdateEngine#applyPayload}.</p>
430 */
431 private static class UpdateData {
432 private final PayloadSpec mPayload;
433 private final ImmutableList<String> mExtraProperties;
434
435 public static Builder builder() {
436 return new Builder();
437 }
438
439 UpdateData(Builder builder) {
440 this.mPayload = builder.mPayload;
441 this.mExtraProperties = ImmutableList.copyOf(builder.mExtraProperties);
442 }
443
444 public PayloadSpec getPayload() {
445 return mPayload;
446 }
447
448 public ImmutableList<String> getExtraProperties() {
449 return mExtraProperties;
450 }
451
452 public Builder toBuilder() {
453 return builder()
454 .setPayload(mPayload)
455 .setExtraProperties(mExtraProperties);
456 }
457
458 static class Builder {
459 private PayloadSpec mPayload;
460 private List<String> mExtraProperties;
461
462 public Builder setPayload(PayloadSpec payload) {
463 this.mPayload = payload;
464 return this;
465 }
466
467 public Builder setExtraProperties(List<String> extraProperties) {
468 this.mExtraProperties = new ArrayList<>(extraProperties);
469 return this;
470 }
471
472 public Builder addExtraProperty(String property) {
473 if (this.mExtraProperties == null) {
474 this.mExtraProperties = new ArrayList<>();
475 }
476 this.mExtraProperties.add(property);
477 return this;
478 }
479
480 public UpdateData build() {
481 return new UpdateData(this);
482 }
483 }
484 }
485
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700486}