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