DownloadPackageTask.java revision 72023ca7b7a818fa33e0303baf6f5baef05b5f1b
1/* 2 * Copyright 2014, 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 */ 16package com.android.managedprovisioning.task; 17 18import android.app.DownloadManager; 19import android.app.DownloadManager.Query; 20import android.app.DownloadManager.Request; 21import android.content.BroadcastReceiver; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.content.pm.PackageInfo; 26import android.content.pm.PackageManager; 27import android.content.pm.PackageManager.NameNotFoundException; 28import android.content.pm.Signature; 29import android.database.Cursor; 30import android.net.Uri; 31import android.text.TextUtils; 32 33import com.android.managedprovisioning.NetworkMonitor; 34import com.android.managedprovisioning.ProvisionLogger; 35import com.android.managedprovisioning.ProvisioningParams.PackageDownloadInfo; 36import com.android.managedprovisioning.common.Utils; 37 38import java.io.InputStream; 39import java.io.IOException; 40import java.io.File; 41import java.io.FileInputStream; 42import java.security.MessageDigest; 43import java.security.NoSuchAlgorithmException; 44import java.util.Arrays; 45import java.util.HashSet; 46import java.util.LinkedList; 47import java.util.List; 48import java.util.Set; 49 50/** 51 * Downloads all packages that were added. Also verifies that the downloaded files are the ones that 52 * are expected. 53 */ 54public class DownloadPackageTask { 55 private static final boolean DEBUG = false; // To control logging. 56 57 public static final int ERROR_HASH_MISMATCH = 0; 58 public static final int ERROR_DOWNLOAD_FAILED = 1; 59 public static final int ERROR_OTHER = 2; 60 61 private static final String SHA1_TYPE = "SHA-1"; 62 private static final String SHA256_TYPE = "SHA-256"; 63 64 private final Context mContext; 65 private final Callback mCallback; 66 private BroadcastReceiver mReceiver; 67 private final DownloadManager mDlm; 68 private final PackageManager mPm; 69 private int mFileNumber = 0; 70 71 private final Utils mUtils = new Utils(); 72 73 private Set<DownloadStatusInfo> mDownloads; 74 75 public DownloadPackageTask (Context context, Callback callback) { 76 mCallback = callback; 77 mContext = context; 78 mDlm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); 79 mPm = context.getPackageManager(); 80 81 mDownloads = new HashSet<DownloadStatusInfo>(); 82 } 83 84 public void addDownloadIfNecessary(String packageName, PackageDownloadInfo downloadInfo, 85 String label) { 86 if (!TextUtils.isEmpty(downloadInfo.location) && mUtils.packageRequiresUpdate(packageName, 87 downloadInfo.minVersion, mContext)) { 88 mDownloads.add(new DownloadStatusInfo(downloadInfo, label)); 89 } 90 } 91 92 public void run() { 93 if (mDownloads.size() == 0) { 94 mCallback.onSuccess(); 95 return; 96 } 97 if (!NetworkMonitor.isConnectedToNetwork(mContext)) { 98 ProvisionLogger.loge("DownloadPackageTask: not connected to the network, can't download" 99 + " the package"); 100 mCallback.onError(ERROR_OTHER); 101 } 102 mReceiver = createDownloadReceiver(); 103 mContext.registerReceiver(mReceiver, 104 new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); 105 106 DownloadManager dm = (DownloadManager) mContext 107 .getSystemService(Context.DOWNLOAD_SERVICE); 108 for (DownloadStatusInfo info : mDownloads) { 109 if (DEBUG) { 110 ProvisionLogger.logd("Starting download from " + 111 info.mPackageDownloadInfo.location); 112 } 113 114 Request request = new Request(Uri.parse(info.mPackageDownloadInfo.location)); 115 // All we want is to have a different file for each apk 116 String path = mContext.getExternalFilesDir(null) 117 + "/download_cache/managed_provisioning_downloaded_app_" + mFileNumber + ".apk"; 118 mFileNumber++; 119 File downloadedFile = new File(path); 120 downloadedFile.getParentFile().mkdirs(); // If the folder doesn't exists it is created 121 request.setDestinationUri(Uri.fromFile(downloadedFile)); 122 info.mLocation = path; 123 if (info.mPackageDownloadInfo.cookieHeader != null) { 124 request.addRequestHeader("Cookie", info.mPackageDownloadInfo.cookieHeader); 125 if (DEBUG) { 126 ProvisionLogger.logd("Downloading with http cookie header: " 127 + info.mPackageDownloadInfo.cookieHeader); 128 } 129 } 130 info.mDownloadId = dm.enqueue(request); 131 } 132 } 133 134 private BroadcastReceiver createDownloadReceiver() { 135 return new BroadcastReceiver() { 136 /** 137 * Whenever the download manager finishes a download, record the successful download for 138 * the corresponding DownloadStatusInfo. 139 */ 140 @Override 141 public void onReceive(Context context, Intent intent) { 142 if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { 143 Query q = new Query(); 144 for (DownloadStatusInfo info : mDownloads) { 145 q.setFilterById(info.mDownloadId); 146 Cursor c = mDlm.query(q); 147 if (c.moveToFirst()) { 148 long downloadId = 149 c.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)); 150 int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS); 151 if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) { 152 c.close(); 153 onDownloadSuccess(downloadId); 154 } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)){ 155 int reason = c.getInt( 156 c.getColumnIndex(DownloadManager.COLUMN_REASON)); 157 c.close(); 158 onDownloadFail(reason); 159 } 160 } 161 } 162 } 163 } 164 }; 165 } 166 167 /** 168 * For a given successful download, check that the downloaded file is the expected file. 169 * If the package hash is provided then that is used, otherwise a signature hash is used. 170 * Then check if this was the last file the task had to download and finish the 171 * DownloadPackageTask if that is the case. 172 * @param downloadId the unique download id for the completed download. 173 * @param location the file location of the downloaded file. 174 */ 175 private void onDownloadSuccess(long downloadId) { 176 DownloadStatusInfo info = null; 177 for (DownloadStatusInfo infoToMatch : mDownloads) { 178 if (downloadId == infoToMatch.mDownloadId) { 179 info = infoToMatch; 180 } 181 } 182 if (info == null || info.mDoneDownloading) { 183 // DownloadManager can send success more than once. Only act first time. 184 return; 185 } else { 186 info.mDoneDownloading = true; 187 } 188 ProvisionLogger.logd("Downloaded succesfully to: " + info.mLocation); 189 190 boolean downloadedContentsCorrect = false; 191 if (info.mPackageDownloadInfo.packageChecksum.length > 0) { 192 downloadedContentsCorrect = doesPackageHashMatch(info); 193 } else if (info.mPackageDownloadInfo.signatureChecksum.length > 0) { 194 downloadedContentsCorrect = doesASignatureHashMatch(info); 195 } 196 197 if (downloadedContentsCorrect) { 198 info.mSuccess = true; 199 checkSuccess(); 200 } else { 201 mCallback.onError(ERROR_HASH_MISMATCH); 202 } 203 } 204 205 /** 206 * Check whether package hash of downloaded file matches the hash given in DownloadStatusInfo. 207 * By default, SHA-256 is used to verify the file hash. 208 * If mPackageDownloadInfo.packageChecksumSupportsSha1 == true, SHA-1 hash is also supported for 209 * backwards compatibility. 210 */ 211 private boolean doesPackageHashMatch(DownloadStatusInfo info) { 212 byte[] packageSha256Hash, packageSha1Hash = null; 213 214 ProvisionLogger.logd("Checking file hash of entire apk file."); 215 packageSha256Hash = computeHashOfFile(info.mLocation, SHA256_TYPE); 216 if (packageSha256Hash == null) { 217 // Error should have been reported in computeHashOfFile(). 218 return false; 219 } 220 221 if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha256Hash)) { 222 return true; 223 } 224 225 // Fall back to SHA-1 226 if (info.mPackageDownloadInfo.packageChecksumSupportsSha1) { 227 packageSha1Hash = computeHashOfFile(info.mLocation, SHA1_TYPE); 228 if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha1Hash)) { 229 return true; 230 } 231 } 232 233 ProvisionLogger.loge("Provided hash does not match file hash."); 234 ProvisionLogger.loge("Hash provided by programmer: " 235 + mUtils.byteArrayToString(info.mPackageDownloadInfo.packageChecksum)); 236 ProvisionLogger.loge("SHA-256 Hash computed from file: " + mUtils.byteArrayToString( 237 packageSha256Hash)); 238 if (packageSha1Hash != null) { 239 ProvisionLogger.loge("SHA-1 Hash computed from file: " + mUtils.byteArrayToString( 240 packageSha1Hash)); 241 } 242 return false; 243 } 244 245 private boolean doesASignatureHashMatch(DownloadStatusInfo info) { 246 // Check whether a signature hash of downloaded apk matches the hash given in constructor. 247 ProvisionLogger.logd("Checking " + SHA256_TYPE 248 + "-hashes of all signatures of downloaded package."); 249 List<byte[]> sigHashes = computeHashesOfAllSignatures(info.mLocation); 250 if (sigHashes == null) { 251 // Error should have been reported in computeHashesOfAllSignatures(). 252 return false; 253 } 254 if (sigHashes.isEmpty()) { 255 ProvisionLogger.loge("Downloaded package does not have any signatures."); 256 return false; 257 } 258 for (byte[] sigHash : sigHashes) { 259 if (Arrays.equals(sigHash, info.mPackageDownloadInfo.signatureChecksum)) { 260 return true; 261 } 262 } 263 264 ProvisionLogger.loge("Provided hash does not match any signature hash."); 265 ProvisionLogger.loge("Hash provided by programmer: " 266 + mUtils.byteArrayToString(info.mPackageDownloadInfo.signatureChecksum)); 267 ProvisionLogger.loge("Hashes computed from package signatures: "); 268 for (byte[] sigHash : sigHashes) { 269 ProvisionLogger.loge(mUtils.byteArrayToString(sigHash)); 270 } 271 272 return false; 273 } 274 275 private void checkSuccess() { 276 for (DownloadStatusInfo info : mDownloads) { 277 if (!info.mSuccess) { 278 return; 279 } 280 } 281 mCallback.onSuccess(); 282 } 283 284 private void onDownloadFail(int errorCode) { 285 ProvisionLogger.loge("Downloading package failed."); 286 ProvisionLogger.loge("COLUMN_REASON in DownloadManager response has value: " 287 + errorCode); 288 mCallback.onError(ERROR_DOWNLOAD_FAILED); 289 } 290 291 private byte[] computeHashOfFile(String fileLocation, String hashType) { 292 InputStream fis = null; 293 MessageDigest md; 294 byte hash[] = null; 295 try { 296 md = MessageDigest.getInstance(hashType); 297 } catch (NoSuchAlgorithmException e) { 298 ProvisionLogger.loge("Hashing algorithm " + hashType + " not supported.", e); 299 mCallback.onError(ERROR_OTHER); 300 return null; 301 } 302 try { 303 fis = new FileInputStream(fileLocation); 304 305 byte[] buffer = new byte[256]; 306 int n = 0; 307 while (n != -1) { 308 n = fis.read(buffer); 309 if (n > 0) { 310 md.update(buffer, 0, n); 311 } 312 } 313 hash = md.digest(); 314 } catch (IOException e) { 315 ProvisionLogger.loge("IO error.", e); 316 mCallback.onError(ERROR_OTHER); 317 } finally { 318 // Close input stream quietly. 319 try { 320 if (fis != null) { 321 fis.close(); 322 } 323 } catch (IOException e) { 324 // Ignore. 325 } 326 } 327 return hash; 328 } 329 330 public String getDownloadedPackageLocation(String label) { 331 for (DownloadStatusInfo info : mDownloads) { 332 if (info.mLabel.equals(label)) { 333 return info.mLocation; 334 } 335 } 336 return ""; 337 } 338 339 private List<byte[]> computeHashesOfAllSignatures(String packageArchiveLocation) { 340 PackageInfo info = mPm.getPackageArchiveInfo(packageArchiveLocation, 341 PackageManager.GET_SIGNATURES); 342 343 List<byte[]> hashes = new LinkedList<byte[]>(); 344 Signature signatures[] = info.signatures; 345 try { 346 for (Signature signature : signatures) { 347 byte[] hash = computeHashOfByteArray(signature.toByteArray()); 348 hashes.add(hash); 349 } 350 } catch (NoSuchAlgorithmException e) { 351 ProvisionLogger.loge("Hashing algorithm " + SHA256_TYPE + " not supported.", e); 352 mCallback.onError(ERROR_OTHER); 353 return null; 354 } 355 return hashes; 356 } 357 358 private byte[] computeHashOfByteArray(byte[] bytes) throws NoSuchAlgorithmException { 359 MessageDigest md = MessageDigest.getInstance(SHA256_TYPE); 360 md.update(bytes, 0, bytes.length); 361 return md.digest(); 362 } 363 364 public void cleanUp() { 365 if (mReceiver != null) { 366 //Unregister receiver. 367 mContext.unregisterReceiver(mReceiver); 368 mReceiver = null; 369 } 370 371 //Remove download. 372 DownloadManager dm = (DownloadManager) mContext 373 .getSystemService(Context.DOWNLOAD_SERVICE); 374 for (DownloadStatusInfo info : mDownloads) { 375 boolean removeSuccess = dm.remove(info.mDownloadId) == 1; 376 if (removeSuccess) { 377 ProvisionLogger.logd("Successfully removed installer file."); 378 } else { 379 ProvisionLogger.loge("Could not remove installer file."); 380 // Ignore this error. Failing cleanup should not stop provisioning flow. 381 } 382 } 383 } 384 385 public abstract static class Callback { 386 public abstract void onSuccess(); 387 public abstract void onError(int errorCode); 388 } 389 390 private static class DownloadStatusInfo { 391 public final PackageDownloadInfo mPackageDownloadInfo; 392 public final String mLabel; 393 public long mDownloadId; 394 public String mLocation; // Location where the package is downloaded to. 395 public boolean mDoneDownloading; 396 public boolean mSuccess; 397 398 public DownloadStatusInfo(PackageDownloadInfo packageDownloadInfo,String label) { 399 mPackageDownloadInfo = packageDownloadInfo; 400 mLabel = label; 401 mDoneDownloading = false; 402 } 403 } 404} 405