blob: 29eb13da7f4d04549baea4689c59837fdea537bb [file] [log] [blame]
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -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.services;
18
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -070019import static com.example.android.systemupdatersample.util.PackageFiles.COMPATIBILITY_ZIP_FILE_NAME;
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070020import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR;
21import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
22import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME;
23
24import android.app.IntentService;
25import android.content.Context;
26import android.content.Intent;
27import android.os.Bundle;
28import android.os.Handler;
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -070029import android.os.RecoverySystem;
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070030import android.os.ResultReceiver;
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080031import android.os.UpdateEngine;
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070032import android.util.Log;
33
34import com.example.android.systemupdatersample.PayloadSpec;
35import com.example.android.systemupdatersample.UpdateConfig;
36import com.example.android.systemupdatersample.util.FileDownloader;
37import com.example.android.systemupdatersample.util.PackageFiles;
38import com.example.android.systemupdatersample.util.PayloadSpecs;
39import com.example.android.systemupdatersample.util.UpdateConfigs;
40import com.google.common.collect.ImmutableSet;
41
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -070042import java.io.File;
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070043import java.io.IOException;
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -070044import java.nio.file.Files;
Zhomart Mukhamejanovc18d4882018-12-14 10:36:20 -080045import java.nio.file.Path;
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070046import java.nio.file.Paths;
Zhomart Mukhamejanovc18d4882018-12-14 10:36:20 -080047import java.util.Arrays;
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070048import java.util.Optional;
49
50/**
51 * This IntentService will download/extract the necessary files from the package zip
52 * without downloading the whole package. And it constructs {@link PayloadSpec}.
53 * All this work required to install streaming A/B updates.
54 *
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080055 * PrepareUpdateService runs on it's own thread. It will notify activity
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070056 * using interface {@link UpdateResultCallback} when update is ready to install.
57 */
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080058public class PrepareUpdateService extends IntentService {
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070059
60 /**
61 * UpdateResultCallback result codes.
62 */
63 public static final int RESULT_CODE_SUCCESS = 0;
64 public static final int RESULT_CODE_ERROR = 1;
65
66 /**
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080067 * Extra params that will be sent to IntentService.
68 */
69 public static final String EXTRA_PARAM_CONFIG = "config";
70 public static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
71
72 /**
73 * This interface is used to send results from {@link PrepareUpdateService} to
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070074 * {@code MainActivity}.
75 */
76 public interface UpdateResultCallback {
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070077 /**
78 * Invoked when files are downloaded and payload spec is constructed.
79 *
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080080 * @param resultCode result code, values are defined in {@link PrepareUpdateService}
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070081 * @param payloadSpec prepared payload spec for streaming update
82 */
83 void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
84 }
85
86 /**
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080087 * Starts PrepareUpdateService.
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070088 *
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080089 * @param context application context
90 * @param config update config
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070091 * @param resultCallback callback that will be called when the update is ready to be installed
92 */
93 public static void startService(Context context,
94 UpdateConfig config,
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080095 Handler handler,
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -070096 UpdateResultCallback resultCallback) {
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -080097 Log.d(TAG, "Starting PrepareUpdateService");
98 ResultReceiver receiver = new CallbackResultReceiver(handler, resultCallback);
99 Intent intent = new Intent(context, PrepareUpdateService.class);
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700100 intent.putExtra(EXTRA_PARAM_CONFIG, config);
101 intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
102 context.startService(intent);
103 }
104
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800105 public PrepareUpdateService() {
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700106 super(TAG);
107 }
108
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800109 private static final String TAG = "PrepareUpdateService";
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700110
111 /**
112 * The files that should be downloaded before streaming.
113 */
114 private static final ImmutableSet<String> PRE_STREAMING_FILES_SET =
115 ImmutableSet.of(
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800116 PackageFiles.CARE_MAP_FILE_NAME,
117 PackageFiles.COMPATIBILITY_ZIP_FILE_NAME,
118 PackageFiles.METADATA_FILE_NAME,
119 PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700120 );
121
Zhomart Mukhamejanovb0361ff2018-05-18 10:22:13 -0700122 private final PayloadSpecs mPayloadSpecs = new PayloadSpecs();
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800123 private final UpdateEngine mUpdateEngine = new UpdateEngine();
Zhomart Mukhamejanovb0361ff2018-05-18 10:22:13 -0700124
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700125 @Override
126 protected void onHandleIntent(Intent intent) {
127 Log.d(TAG, "On handle intent is called");
128 UpdateConfig config = intent.getParcelableExtra(EXTRA_PARAM_CONFIG);
129 ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER);
130
131 try {
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700132 PayloadSpec spec = execute(config);
133 resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec));
134 } catch (Exception e) {
135 Log.e(TAG, "Failed to prepare streaming update", e);
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700136 resultReceiver.send(RESULT_CODE_ERROR, null);
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700137 }
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700138 }
139
140 /**
141 * 1. Downloads files for streaming updates.
142 * 2. Makes sure required files are present.
143 * 3. Checks OTA package compatibility with the device.
144 * 4. Constructs {@link PayloadSpec} for streaming update.
145 */
Zhomart Mukhamejanovb0361ff2018-05-18 10:22:13 -0700146 private PayloadSpec execute(UpdateConfig config)
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700147 throws IOException, PreparationFailedException {
148
Zhomart Mukhamejanovc18d4882018-12-14 10:36:20 -0800149 if (config.getAbConfig().getVerifyPayloadMetadata()) {
150 Log.i(TAG, "Verifying payload metadata with UpdateEngine.");
151 if (!verifyPayloadMetadata(config)) {
152 throw new PreparationFailedException("Payload metadata is not compatible");
153 }
154 }
155
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800156 if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
157 return mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
158 }
159
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700160 downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700161
162 Optional<UpdateConfig.PackageFile> payloadBinary =
163 UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
164
165 if (!payloadBinary.isPresent()) {
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700166 throw new PreparationFailedException(
167 "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700168 }
169
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700170 if (!UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config).isPresent()
171 || !Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile().exists()) {
172 throw new IOException(PAYLOAD_PROPERTIES_FILE_NAME + " not found");
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700173 }
174
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700175 File compatibilityFile = Paths.get(OTA_PACKAGE_DIR, COMPATIBILITY_ZIP_FILE_NAME).toFile();
176 if (compatibilityFile.isFile()) {
177 Log.i(TAG, "Verifying OTA package for compatibility with the device");
178 if (!verifyPackageCompatibility(compatibilityFile)) {
179 throw new PreparationFailedException(
180 "OTA package is not compatible with this device");
181 }
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700182 }
183
Zhomart Mukhamejanovb0361ff2018-05-18 10:22:13 -0700184 return mPayloadSpecs.forStreaming(config.getUrl(),
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700185 payloadBinary.get().getOffset(),
186 payloadBinary.get().getSize(),
187 Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile());
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700188 }
189
190 /**
Zhomart Mukhamejanovc18d4882018-12-14 10:36:20 -0800191 * Downloads only payload_metadata.bin and verifies with
192 * {@link UpdateEngine#verifyPayloadMetadata}.
193 * Returns {@code true} if the payload is verified or the result is unknown because of
194 * exception from UpdateEngine.
195 * By downloading only small portion of the package, it allows to verify if UpdateEngine
196 * will install the update.
197 */
198 private boolean verifyPayloadMetadata(UpdateConfig config) {
199 Optional<UpdateConfig.PackageFile> metadataPackageFile =
200 Arrays.stream(config.getAbConfig().getPropertyFiles())
201 .filter(p -> p.getFilename().equals(
202 PackageFiles.PAYLOAD_METADATA_FILE_NAME))
203 .findFirst();
204 if (!metadataPackageFile.isPresent()) {
205 Log.w(TAG, String.format("ab_config.property_files doesn't contain %s",
206 PackageFiles.PAYLOAD_METADATA_FILE_NAME));
207 return true;
208 }
209 Path metadataPath = Paths.get(OTA_PACKAGE_DIR, PackageFiles.PAYLOAD_METADATA_FILE_NAME);
210 try {
211 Files.deleteIfExists(metadataPath);
212 FileDownloader d = new FileDownloader(
213 config.getUrl(),
214 metadataPackageFile.get().getOffset(),
215 metadataPackageFile.get().getSize(),
216 metadataPath.toFile());
217 d.download();
218 } catch (IOException e) {
219 Log.w(TAG, String.format("Downloading %s from %s failed",
220 PackageFiles.PAYLOAD_METADATA_FILE_NAME,
221 config.getUrl()), e);
222 return true;
223 }
224 try {
225 return mUpdateEngine.verifyPayloadMetadata(metadataPath.toAbsolutePath().toString());
226 } catch (Exception e) {
227 Log.w(TAG, "UpdateEngine#verifyPayloadMetadata failed", e);
228 return true;
229 }
230 }
231
232 /**
Zhomart Mukhamejanov88712f72018-08-21 12:19:02 -0700233 * Downloads files defined in {@link UpdateConfig#getAbConfig()}
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700234 * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
235 * in directory {@code dir}.
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800236 *
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700237 * @throws IOException when can't download a file
238 */
Zhomart Mukhamejanovb0361ff2018-05-18 10:22:13 -0700239 private void downloadPreStreamingFiles(UpdateConfig config, String dir)
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700240 throws IOException {
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700241 Log.d(TAG, "Deleting existing files from " + dir);
242 for (String file : PRE_STREAMING_FILES_SET) {
243 Files.deleteIfExists(Paths.get(OTA_PACKAGE_DIR, file));
244 }
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700245 Log.d(TAG, "Downloading files to " + dir);
Zhomart Mukhamejanov88712f72018-08-21 12:19:02 -0700246 for (UpdateConfig.PackageFile file : config.getAbConfig().getPropertyFiles()) {
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700247 if (PRE_STREAMING_FILES_SET.contains(file.getFilename())) {
248 Log.d(TAG, "Downloading file " + file.getFilename());
249 FileDownloader downloader = new FileDownloader(
250 config.getUrl(),
251 file.getOffset(),
252 file.getSize(),
253 Paths.get(dir, file.getFilename()).toFile());
254 downloader.download();
255 }
256 }
257 }
258
259 /**
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700260 * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME}
261 * @return true if OTA package is compatible with this device
262 */
Zhomart Mukhamejanovb0361ff2018-05-18 10:22:13 -0700263 private boolean verifyPackageCompatibility(File file) {
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700264 try {
265 return RecoverySystem.verifyPackageCompatibility(file);
266 } catch (IOException e) {
267 Log.e(TAG, "Failed to verify package compatibility", e);
268 return false;
269 }
270 }
271
272 /**
Zhomart Mukhamejanovbc077752018-12-14 09:36:32 -0800273 * Used by {@link PrepareUpdateService} to pass {@link PayloadSpec}
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700274 * to {@link UpdateResultCallback#onReceiveResult}.
275 */
276 private static class CallbackResultReceiver extends ResultReceiver {
277
278 static Bundle createBundle(PayloadSpec payloadSpec) {
279 Bundle b = new Bundle();
280 b.putSerializable(BUNDLE_PARAM_PAYLOAD_SPEC, payloadSpec);
281 return b;
282 }
283
284 private static final String BUNDLE_PARAM_PAYLOAD_SPEC = "payload-spec";
285
286 private UpdateResultCallback mUpdateResultCallback;
287
288 CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) {
289 super(handler);
290 this.mUpdateResultCallback = updateResultCallback;
291 }
292
293 @Override
294 protected void onReceiveResult(int resultCode, Bundle resultData) {
295 PayloadSpec payloadSpec = null;
296 if (resultCode == RESULT_CODE_SUCCESS) {
297 payloadSpec = (PayloadSpec) resultData.getSerializable(BUNDLE_PARAM_PAYLOAD_SPEC);
298 }
299 mUpdateResultCallback.onReceiveResult(resultCode, payloadSpec);
300 }
301 }
302
Zhomart Mukhamejanov46a51ac2018-05-09 09:53:45 -0700303 private static class PreparationFailedException extends Exception {
304 PreparationFailedException(String message) {
305 super(message);
306 }
307 }
308
Zhomart Mukhamejanov0dd5a832018-04-23 11:38:54 -0700309}