blob: caa3f7315730c3db3cbc698d9bcbc0098309ab23 [file] [log] [blame]
/*
* Copyright 2014, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.managedprovisioning.task;
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import com.android.managedprovisioning.NetworkMonitor;
import com.android.managedprovisioning.ProvisionLogger;
import com.android.managedprovisioning.common.Utils;
import com.android.managedprovisioning.model.PackageDownloadInfo;
import java.io.InputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Downloads all packages that were added. Also verifies that the downloaded files are the ones that
* are expected.
*/
public class DownloadPackageTask {
private static final boolean DEBUG = false; // To control logging.
public static final int ERROR_HASH_MISMATCH = 0;
public static final int ERROR_DOWNLOAD_FAILED = 1;
public static final int ERROR_OTHER = 2;
private static final String SHA1_TYPE = "SHA-1";
private static final String SHA256_TYPE = "SHA-256";
private final Context mContext;
private final Callback mCallback;
private BroadcastReceiver mReceiver;
private final DownloadManager mDlm;
private final PackageManager mPm;
private int mFileNumber = 0;
private final Utils mUtils = new Utils();
private Set<DownloadStatusInfo> mDownloads;
public DownloadPackageTask (Context context, Callback callback) {
mCallback = callback;
mContext = context;
mDlm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
mDlm.setAccessFilename(true);
mPm = context.getPackageManager();
mDownloads = new HashSet<DownloadStatusInfo>();
}
public void addDownloadIfNecessary(
String packageName, PackageDownloadInfo downloadInfo, String label) {
if (downloadInfo != null
&& mUtils.packageRequiresUpdate(packageName, downloadInfo.minVersion, mContext)) {
mDownloads.add(new DownloadStatusInfo(downloadInfo, label));
}
}
public void run() {
if (mDownloads.size() == 0) {
mCallback.onSuccess();
return;
}
if (!mUtils.isConnectedToNetwork(mContext)) {
ProvisionLogger.loge("DownloadPackageTask: not connected to the network, can't download"
+ " the package");
mCallback.onError(ERROR_OTHER);
}
mReceiver = createDownloadReceiver();
mContext.registerReceiver(mReceiver,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
DownloadManager dm = (DownloadManager) mContext
.getSystemService(Context.DOWNLOAD_SERVICE);
for (DownloadStatusInfo info : mDownloads) {
if (DEBUG) {
ProvisionLogger.logd("Starting download from " +
info.mPackageDownloadInfo.location);
}
Request request = new Request(Uri.parse(info.mPackageDownloadInfo.location));
// All we want is to have a different file for each apk
// Note that the apk may not actually be downloaded to this path. This could happen if
// this file already exists.
String path = mContext.getExternalFilesDir(null)
+ "/download_cache/managed_provisioning_downloaded_app_" + mFileNumber + ".apk";
mFileNumber++;
File downloadedFile = new File(path);
downloadedFile.getParentFile().mkdirs(); // If the folder doesn't exists it is created
request.setDestinationUri(Uri.fromFile(downloadedFile));
if (info.mPackageDownloadInfo.cookieHeader != null) {
request.addRequestHeader("Cookie", info.mPackageDownloadInfo.cookieHeader);
if (DEBUG) {
ProvisionLogger.logd("Downloading with http cookie header: "
+ info.mPackageDownloadInfo.cookieHeader);
}
}
info.mDownloadId = dm.enqueue(request);
}
}
private BroadcastReceiver createDownloadReceiver() {
return new BroadcastReceiver() {
/**
* Whenever the download manager finishes a download, record the successful download for
* the corresponding DownloadStatusInfo.
*/
@Override
public void onReceive(Context context, Intent intent) {
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
Query q = new Query();
for (DownloadStatusInfo info : mDownloads) {
q.setFilterById(info.mDownloadId);
Cursor c = mDlm.query(q);
if (c.moveToFirst()) {
long downloadId =
c.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID));
String filePath = c.getString(c.getColumnIndex(
DownloadManager.COLUMN_LOCAL_FILENAME));
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
c.close();
onDownloadSuccess(downloadId, filePath);
} else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)){
int reason = c.getInt(
c.getColumnIndex(DownloadManager.COLUMN_REASON));
c.close();
onDownloadFail(reason);
}
}
}
}
}
};
}
/**
* For a given successful download, check that the downloaded file is the expected file.
* If the package hash is provided then that is used, otherwise a signature hash is used.
* Then check if this was the last file the task had to download and finish the
* DownloadPackageTask if that is the case.
* @param downloadId the unique download id for the completed download.
* @param location the file location of the downloaded file.
*/
private void onDownloadSuccess(long downloadId, String filePath) {
DownloadStatusInfo info = null;
for (DownloadStatusInfo infoToMatch : mDownloads) {
if (downloadId == infoToMatch.mDownloadId) {
info = infoToMatch;
}
}
if (info == null || info.mDoneDownloading) {
// DownloadManager can send success more than once. Only act first time.
return;
} else {
info.mDoneDownloading = true;
info.mLocation = filePath;
}
ProvisionLogger.logd("Downloaded succesfully to: " + info.mLocation);
boolean downloadedContentsCorrect = false;
if (info.mPackageDownloadInfo.packageChecksum.length > 0) {
downloadedContentsCorrect = doesPackageHashMatch(info);
} else if (info.mPackageDownloadInfo.signatureChecksum.length > 0) {
downloadedContentsCorrect = doesASignatureHashMatch(info);
}
if (downloadedContentsCorrect) {
info.mSuccess = true;
checkSuccess();
} else {
mCallback.onError(ERROR_HASH_MISMATCH);
}
}
/**
* Check whether package hash of downloaded file matches the hash given in DownloadStatusInfo.
* By default, SHA-256 is used to verify the file hash.
* If mPackageDownloadInfo.packageChecksumSupportsSha1 == true, SHA-1 hash is also supported for
* backwards compatibility.
*/
private boolean doesPackageHashMatch(DownloadStatusInfo info) {
byte[] packageSha256Hash, packageSha1Hash = null;
ProvisionLogger.logd("Checking file hash of entire apk file.");
packageSha256Hash = computeHashOfFile(info.mLocation, SHA256_TYPE);
if (packageSha256Hash == null) {
// Error should have been reported in computeHashOfFile().
return false;
}
if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha256Hash)) {
return true;
}
// Fall back to SHA-1
if (info.mPackageDownloadInfo.packageChecksumSupportsSha1) {
packageSha1Hash = computeHashOfFile(info.mLocation, SHA1_TYPE);
if (Arrays.equals(info.mPackageDownloadInfo.packageChecksum, packageSha1Hash)) {
return true;
}
}
ProvisionLogger.loge("Provided hash does not match file hash.");
ProvisionLogger.loge("Hash provided by programmer: "
+ mUtils.byteArrayToString(info.mPackageDownloadInfo.packageChecksum));
ProvisionLogger.loge("SHA-256 Hash computed from file: " + mUtils.byteArrayToString(
packageSha256Hash));
if (packageSha1Hash != null) {
ProvisionLogger.loge("SHA-1 Hash computed from file: " + mUtils.byteArrayToString(
packageSha1Hash));
}
return false;
}
private boolean doesASignatureHashMatch(DownloadStatusInfo info) {
// Check whether a signature hash of downloaded apk matches the hash given in constructor.
ProvisionLogger.logd("Checking " + SHA256_TYPE
+ "-hashes of all signatures of downloaded package.");
List<byte[]> sigHashes = computeHashesOfAllSignatures(info.mLocation);
if (sigHashes == null) {
// Error should have been reported in computeHashesOfAllSignatures().
return false;
}
if (sigHashes.isEmpty()) {
ProvisionLogger.loge("Downloaded package does not have any signatures.");
return false;
}
for (byte[] sigHash : sigHashes) {
if (Arrays.equals(sigHash, info.mPackageDownloadInfo.signatureChecksum)) {
return true;
}
}
ProvisionLogger.loge("Provided hash does not match any signature hash.");
ProvisionLogger.loge("Hash provided by programmer: "
+ mUtils.byteArrayToString(info.mPackageDownloadInfo.signatureChecksum));
ProvisionLogger.loge("Hashes computed from package signatures: ");
for (byte[] sigHash : sigHashes) {
ProvisionLogger.loge(mUtils.byteArrayToString(sigHash));
}
return false;
}
private void checkSuccess() {
for (DownloadStatusInfo info : mDownloads) {
if (!info.mSuccess) {
return;
}
}
mCallback.onSuccess();
}
private void onDownloadFail(int errorCode) {
ProvisionLogger.loge("Downloading package failed.");
ProvisionLogger.loge("COLUMN_REASON in DownloadManager response has value: "
+ errorCode);
mCallback.onError(ERROR_DOWNLOAD_FAILED);
}
private byte[] computeHashOfFile(String fileLocation, String hashType) {
InputStream fis = null;
MessageDigest md;
byte hash[] = null;
try {
md = MessageDigest.getInstance(hashType);
} catch (NoSuchAlgorithmException e) {
ProvisionLogger.loge("Hashing algorithm " + hashType + " not supported.", e);
mCallback.onError(ERROR_OTHER);
return null;
}
try {
fis = new FileInputStream(fileLocation);
byte[] buffer = new byte[256];
int n = 0;
while (n != -1) {
n = fis.read(buffer);
if (n > 0) {
md.update(buffer, 0, n);
}
}
hash = md.digest();
} catch (IOException e) {
ProvisionLogger.loge("IO error.", e);
mCallback.onError(ERROR_OTHER);
} finally {
// Close input stream quietly.
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
// Ignore.
}
}
return hash;
}
public String getDownloadedPackageLocation(String label) {
for (DownloadStatusInfo info : mDownloads) {
if (info.mLabel.equals(label)) {
return info.mLocation;
}
}
return "";
}
private List<byte[]> computeHashesOfAllSignatures(String packageArchiveLocation) {
PackageInfo info = mPm.getPackageArchiveInfo(packageArchiveLocation,
PackageManager.GET_SIGNATURES);
if (info == null) {
ProvisionLogger.loge("Unable to get package archive info from "
+ packageArchiveLocation);
mCallback.onError(ERROR_OTHER);
return null;
}
List<byte[]> hashes = new LinkedList<byte[]>();
Signature signatures[] = info.signatures;
try {
for (Signature signature : signatures) {
byte[] hash = computeHashOfByteArray(signature.toByteArray());
hashes.add(hash);
}
} catch (NoSuchAlgorithmException e) {
ProvisionLogger.loge("Hashing algorithm " + SHA256_TYPE + " not supported.", e);
mCallback.onError(ERROR_OTHER);
return null;
}
return hashes;
}
private byte[] computeHashOfByteArray(byte[] bytes) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(SHA256_TYPE);
md.update(bytes, 0, bytes.length);
return md.digest();
}
public void cleanUp() {
if (mReceiver != null) {
//Unregister receiver.
mContext.unregisterReceiver(mReceiver);
mReceiver = null;
}
//Remove download.
DownloadManager dm = (DownloadManager) mContext
.getSystemService(Context.DOWNLOAD_SERVICE);
for (DownloadStatusInfo info : mDownloads) {
boolean removeSuccess = dm.remove(info.mDownloadId) == 1;
if (removeSuccess) {
ProvisionLogger.logd("Successfully removed installer file.");
} else {
ProvisionLogger.loge("Could not remove installer file.");
// Ignore this error. Failing cleanup should not stop provisioning flow.
}
}
}
public abstract static class Callback {
public abstract void onSuccess();
public abstract void onError(int errorCode);
}
private static class DownloadStatusInfo {
public final PackageDownloadInfo mPackageDownloadInfo;
public final String mLabel;
public long mDownloadId;
public String mLocation; // Location where the package is downloaded to.
public boolean mDoneDownloading;
public boolean mSuccess;
public DownloadStatusInfo(PackageDownloadInfo packageDownloadInfo,String label) {
mPackageDownloadInfo = packageDownloadInfo;
mLabel = label;
mDoneDownloading = false;
}
}
}