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