blob: c370a4eb547525c1c4d4761eb5b3ff49e47d8343 [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 Mukhamejanov6f26e712018-05-18 10:15:31 -070029import com.google.common.util.concurrent.AtomicDouble;
30
31import java.io.IOException;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.Optional;
35import java.util.concurrent.atomic.AtomicBoolean;
36import java.util.concurrent.atomic.AtomicInteger;
37import java.util.function.DoubleConsumer;
38import java.util.function.IntConsumer;
39
40/**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070041 * Manages the update flow. It has its own state (in memory), separate from
42 * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}.
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070043 */
44public class UpdateManager {
45
46 private static final String TAG = "UpdateManager";
47
48 /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */
49 private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
50 + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
51
52 private final UpdateEngine mUpdateEngine;
53 private final PayloadSpecs mPayloadSpecs;
54
55 private AtomicInteger mUpdateEngineStatus =
56 new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
57 private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN);
58 private AtomicDouble mProgress = new AtomicDouble(0);
59
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -070060 private AtomicInteger mState = new AtomicInteger(UpdaterStates.IDLE);
61
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -070062 private final UpdateManager.UpdateEngineCallbackImpl
63 mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl();
64
65 private PayloadSpec mLastPayloadSpec;
66 private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
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
75 public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) {
76 this.mUpdateEngine = updateEngine;
77 this.mPayloadSpecs = payloadSpecs;
78 }
79
80 /**
81 * Binds to {@link UpdateEngine}.
82 */
83 public void bind() {
84 this.mUpdateEngine.bind(mUpdateEngineCallback);
85 }
86
87 /**
88 * Unbinds from {@link UpdateEngine}.
89 */
90 public void unbind() {
91 this.mUpdateEngine.unbind();
92 }
93
94 /**
95 * @return a number from {@code 0.0} to {@code 1.0}.
96 */
97 public float getProgress() {
98 return (float) this.mProgress.get();
99 }
100
101 /**
102 * Returns true if manual switching slot is required. Value depends on
103 * the update config {@code ab_config.force_switch_slot}.
104 */
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700105 public boolean isManualSwitchSlotRequired() {
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700106 return mManualSwitchSlotRequired.get();
107 }
108
109 /**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700110 * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one
111 * of the values from {@link UpdaterStates}.
112 *
113 * @param onStateChangeCallback a callback with parameter {@code state}.
114 */
115 public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) {
116 synchronized (mLock) {
117 this.mOnStateChangeCallback = onStateChangeCallback;
118 }
119 }
120
121 private Optional<IntConsumer> getOnStateChangeCallback() {
122 synchronized (mLock) {
123 return mOnStateChangeCallback == null
124 ? Optional.empty()
125 : Optional.of(mOnStateChangeCallback);
126 }
127 }
128
129 /**
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700130 * Sets update engine status update callback. Value of {@code status} will
131 * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
132 *
133 * @param onStatusUpdateCallback a callback with parameter {@code status}.
134 */
135 public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) {
136 synchronized (mLock) {
137 this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback;
138 }
139 }
140
141 private Optional<IntConsumer> getOnEngineStatusUpdateCallback() {
142 synchronized (mLock) {
143 return mOnEngineStatusUpdateCallback == null
144 ? Optional.empty()
145 : Optional.of(mOnEngineStatusUpdateCallback);
146 }
147 }
148
149 /**
150 * Sets update engine payload application complete callback. Value of {@code errorCode} will
151 * be one of the values from {@link UpdateEngine.ErrorCodeConstants}.
152 *
153 * @param onComplete a callback with parameter {@code errorCode}.
154 */
155 public void setOnEngineCompleteCallback(IntConsumer onComplete) {
156 synchronized (mLock) {
157 this.mOnEngineCompleteCallback = onComplete;
158 }
159 }
160
161 private Optional<IntConsumer> getOnEngineCompleteCallback() {
162 synchronized (mLock) {
163 return mOnEngineCompleteCallback == null
164 ? Optional.empty()
165 : Optional.of(mOnEngineCompleteCallback);
166 }
167 }
168
169 /**
170 * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}.
171 *
172 * @param onProgressCallback a callback with parameter {@code progress}.
173 */
174 public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) {
175 synchronized (mLock) {
176 this.mOnProgressUpdateCallback = onProgressCallback;
177 }
178 }
179
180 private Optional<DoubleConsumer> getOnProgressUpdateCallback() {
181 synchronized (mLock) {
182 return mOnProgressUpdateCallback == null
183 ? Optional.empty()
184 : Optional.of(mOnProgressUpdateCallback);
185 }
186 }
187
188 /**
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700189 * Updates {@link this.mState} and if state is changed,
190 * it also notifies {@link this.mOnStateChangeCallback}.
191 */
192 private void setUpdaterState(int updaterState) {
193 int previousState = mState.get();
194 mState.set(updaterState);
195 if (previousState != updaterState) {
196 getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState));
197 }
198 }
199
200 /**
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700201 * Requests update engine to stop any ongoing update. If an update has been applied,
202 * leave it as is.
203 *
204 * <p>Sometimes it's possible that the
205 * update engine would throw an error when the method is called, and the only way to
206 * handle it is to catch the exception.</p>
207 */
208 public void cancelRunningUpdate() {
209 try {
210 mUpdateEngine.cancel();
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700211 setUpdaterState(UpdaterStates.IDLE);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700212 } catch (Exception e) {
213 Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
214 }
215 }
216
217 /**
218 * Resets update engine to IDLE state. If an update has been applied it reverts it.
219 *
220 * <p>Sometimes it's possible that the
221 * update engine would throw an error when the method is called, and the only way to
222 * handle it is to catch the exception.</p>
223 */
224 public void resetUpdate() {
225 try {
226 mUpdateEngine.resetStatus();
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700227 setUpdaterState(UpdaterStates.IDLE);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700228 } catch (Exception e) {
229 Log.w(TAG, "UpdateEngine failed to reset the update", e);
230 }
231 }
232
233 /**
234 * Applies the given update.
235 *
236 * <p>UpdateEngine works asynchronously. This method doesn't wait until
237 * end of the update.</p>
238 */
239 public void applyUpdate(Context context, UpdateConfig config) {
240 mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700241 setUpdaterState(UpdaterStates.RUNNING);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700242
243 if (!config.getAbConfig().getForceSwitchSlot()) {
244 mManualSwitchSlotRequired.set(true);
245 } else {
246 mManualSwitchSlotRequired.set(false);
247 }
248
249 if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
250 applyAbNonStreamingUpdate(config);
251 } else {
252 applyAbStreamingUpdate(context, config);
253 }
254 }
255
256 private void applyAbNonStreamingUpdate(UpdateConfig config) {
257 List<String> extraProperties = prepareExtraProperties(config);
258
259 PayloadSpec payload;
260 try {
261 payload = mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
262 } catch (IOException e) {
263 Log.e(TAG, "Error creating payload spec", e);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700264 setUpdaterState(UpdaterStates.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700265 return;
266 }
267 updateEngineApplyPayload(payload, extraProperties);
268 }
269
270 private void applyAbStreamingUpdate(Context context, UpdateConfig config) {
271 List<String> extraProperties = prepareExtraProperties(config);
272
273 Log.d(TAG, "Starting PrepareStreamingService");
274 PrepareStreamingService.startService(context, config, (code, payloadSpec) -> {
275 if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
276 extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT);
277 config.getStreamingMetadata()
278 .getAuthorization()
279 .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s));
280 updateEngineApplyPayload(payloadSpec, extraProperties);
281 } else {
282 Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700283 setUpdaterState(UpdaterStates.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700284 }
285 });
286 }
287
288 private List<String> prepareExtraProperties(UpdateConfig config) {
289 List<String> extraProperties = new ArrayList<>();
290
291 if (!config.getAbConfig().getForceSwitchSlot()) {
292 // Disable switch slot on reboot, which is enabled by default.
293 // User will enable it manually by clicking "Switch Slot" button on the screen.
294 extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT);
295 }
296 return extraProperties;
297 }
298
299 /**
300 * Applies given payload.
301 *
302 * <p>UpdateEngine works asynchronously. This method doesn't wait until
303 * end of the update.</p>
304 *
305 * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid
306 * payload properties (which come from OTA packages), or failing to set up the network
307 * with the given id.</p>
308 *
309 * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}
310 * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload}
311 */
312 private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) {
313 mLastPayloadSpec = payloadSpec;
314
315 ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties());
316 if (extraProperties != null) {
317 properties.addAll(extraProperties);
318 }
319 try {
320 mUpdateEngine.applyPayload(
321 payloadSpec.getUrl(),
322 payloadSpec.getOffset(),
323 payloadSpec.getSize(),
324 properties.toArray(new String[0]));
325 } catch (Exception e) {
326 Log.e(TAG, "UpdateEngine failed to apply the update", e);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700327 setUpdaterState(UpdaterStates.ERROR);
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700328 }
329 }
330
331 /**
332 * Sets the new slot that has the updated partitions as the active slot,
333 * which device will boot into next time.
334 * This method is only supposed to be called after the payload is applied.
335 *
336 * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size
337 * and payload metadata headers doesn't trigger new update. It can be used to just switch
338 * active A/B slot.
339 *
340 * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
341 * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
342 */
343 public void setSwitchSlotOnReboot() {
344 Log.d(TAG, "setSwitchSlotOnReboot invoked");
345 List<String> extraProperties = new ArrayList<>();
346 // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks.
347 extraProperties.add(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL);
348 // It sets property SWITCH_SLOT_ON_REBOOT=1 by default.
349 // HTTP headers are not required, UpdateEngine is not expected to stream payload.
350 updateEngineApplyPayload(mLastPayloadSpec, extraProperties);
351 }
352
353 private void onStatusUpdate(int status, float progress) {
354 int previousStatus = mUpdateEngineStatus.get();
355 mUpdateEngineStatus.set(status);
356 mProgress.set(progress);
357
358 getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress));
359
360 if (previousStatus != status) {
361 getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status));
362 }
363 }
364
365 private void onPayloadApplicationComplete(int errorCode) {
366 Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode);
367 mEngineErrorCode.set(errorCode);
Zhomart Mukhamejanov8f4059d2018-05-18 10:15:31 -0700368 if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
369 || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
370 setUpdaterState(UpdaterStates.FINISHED);
371 } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
372 setUpdaterState(UpdaterStates.ERROR);
373 }
Zhomart Mukhamejanov6f26e712018-05-18 10:15:31 -0700374
375 getOnEngineCompleteCallback()
376 .ifPresent(callback -> callback.accept(errorCode));
377 }
378
379 /**
380 * Helper class to delegate {@code update_engine} callbacks to UpdateManager
381 */
382 class UpdateEngineCallbackImpl extends UpdateEngineCallback {
383 @Override
384 public void onStatusUpdate(int status, float percent) {
385 UpdateManager.this.onStatusUpdate(status, percent);
386 }
387
388 @Override
389 public void onPayloadApplicationComplete(int errorCode) {
390 UpdateManager.this.onPayloadApplicationComplete(errorCode);
391 }
392 }
393
394}