1 /*
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 
17 package com.example.android.systemupdatersample.services;
18 
19 import static com.example.android.systemupdatersample.util.PackageFiles.COMPATIBILITY_ZIP_FILE_NAME;
20 import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR;
21 import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
22 import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME;
23 
24 import android.app.IntentService;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.RecoverySystem;
30 import android.os.ResultReceiver;
31 import android.os.UpdateEngine;
32 import android.util.Log;
33 
34 import com.example.android.systemupdatersample.PayloadSpec;
35 import com.example.android.systemupdatersample.UpdateConfig;
36 import com.example.android.systemupdatersample.util.FileDownloader;
37 import com.example.android.systemupdatersample.util.PackageFiles;
38 import com.example.android.systemupdatersample.util.PayloadSpecs;
39 import com.example.android.systemupdatersample.util.UpdateConfigs;
40 import com.google.common.collect.ImmutableSet;
41 
42 import java.io.File;
43 import java.io.IOException;
44 import java.nio.file.Files;
45 import java.nio.file.Path;
46 import java.nio.file.Paths;
47 import java.util.Arrays;
48 import 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  *
55  * PrepareUpdateService runs on it's own thread. It will notify activity
56  * using interface {@link UpdateResultCallback} when update is ready to install.
57  */
58 public class PrepareUpdateService extends IntentService {
59 
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     /**
67      * 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
74      * {@code MainActivity}.
75      */
76     public interface UpdateResultCallback {
77         /**
78          * Invoked when files are downloaded and payload spec is constructed.
79          *
80          * @param resultCode  result code, values are defined in {@link PrepareUpdateService}
81          * @param payloadSpec prepared payload spec for streaming update
82          */
onReceiveResult(int resultCode, PayloadSpec payloadSpec)83         void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
84     }
85 
86     /**
87      * Starts PrepareUpdateService.
88      *
89      * @param context        application context
90      * @param config         update config
91      * @param resultCallback callback that will be called when the update is ready to be installed
92      */
startService(Context context, UpdateConfig config, Handler handler, UpdateResultCallback resultCallback)93     public static void startService(Context context,
94             UpdateConfig config,
95             Handler handler,
96             UpdateResultCallback resultCallback) {
97         Log.d(TAG, "Starting PrepareUpdateService");
98         ResultReceiver receiver = new CallbackResultReceiver(handler, resultCallback);
99         Intent intent = new Intent(context, PrepareUpdateService.class);
100         intent.putExtra(EXTRA_PARAM_CONFIG, config);
101         intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
102         context.startService(intent);
103     }
104 
PrepareUpdateService()105     public PrepareUpdateService() {
106         super(TAG);
107     }
108 
109     private static final String TAG = "PrepareUpdateService";
110 
111     /**
112      * The files that should be downloaded before streaming.
113      */
114     private static final ImmutableSet<String> PRE_STREAMING_FILES_SET =
115             ImmutableSet.of(
116                     PackageFiles.CARE_MAP_FILE_NAME,
117                     PackageFiles.COMPATIBILITY_ZIP_FILE_NAME,
118                     PackageFiles.METADATA_FILE_NAME,
119                     PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME
120             );
121 
122     private final PayloadSpecs mPayloadSpecs = new PayloadSpecs();
123     private final UpdateEngine mUpdateEngine = new UpdateEngine();
124 
125     @Override
onHandleIntent(Intent intent)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 {
132             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);
136             resultReceiver.send(RESULT_CODE_ERROR, null);
137         }
138     }
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      */
execute(UpdateConfig config)146     private PayloadSpec execute(UpdateConfig config)
147             throws IOException, PreparationFailedException {
148 
149         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 
156         if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
157             return mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
158         }
159 
160         downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
161 
162         Optional<UpdateConfig.PackageFile> payloadBinary =
163                 UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
164 
165         if (!payloadBinary.isPresent()) {
166             throw new PreparationFailedException(
167                     "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
168         }
169 
170         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");
173         }
174 
175         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             }
182         }
183 
184         return mPayloadSpecs.forStreaming(config.getUrl(),
185                 payloadBinary.get().getOffset(),
186                 payloadBinary.get().getSize(),
187                 Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile());
188     }
189 
190     /**
191      * 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      */
verifyPayloadMetadata(UpdateConfig config)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     /**
233      * Downloads files defined in {@link UpdateConfig#getAbConfig()}
234      * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
235      * in directory {@code dir}.
236      *
237      * @throws IOException when can't download a file
238      */
downloadPreStreamingFiles(UpdateConfig config, String dir)239     private void downloadPreStreamingFiles(UpdateConfig config, String dir)
240             throws IOException {
241         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         }
245         Log.d(TAG, "Downloading files to " + dir);
246         for (UpdateConfig.PackageFile file : config.getAbConfig().getPropertyFiles()) {
247             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     /**
260      * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME}
261      * @return true if OTA package is compatible with this device
262      */
verifyPackageCompatibility(File file)263     private boolean verifyPackageCompatibility(File file) {
264         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     /**
273      * Used by {@link PrepareUpdateService} to pass {@link PayloadSpec}
274      * to {@link UpdateResultCallback#onReceiveResult}.
275      */
276     private static class CallbackResultReceiver extends ResultReceiver {
277 
createBundle(PayloadSpec payloadSpec)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 
CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback)288         CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) {
289             super(handler);
290             this.mUpdateResultCallback = updateResultCallback;
291         }
292 
293         @Override
onReceiveResult(int resultCode, Bundle resultData)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 
303     private static class PreparationFailedException extends Exception {
PreparationFailedException(String message)304         PreparationFailedException(String message) {
305             super(message);
306         }
307     }
308 
309 }
310