| /* |
| * 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.ActivityInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.text.TextUtils; |
| import android.util.Base64; |
| |
| import com.android.managedprovisioning.ProvisionLogger; |
| |
| import java.io.InputStream; |
| import java.io.IOException; |
| import java.io.FileInputStream; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Arrays; |
| |
| /** |
| * Downloads a given file and checks whether its hash matches a given hash to verify that the |
| * intended file was downloaded. |
| */ |
| public class DownloadPackageTask { |
| 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 HASH_TYPE = "SHA-1"; |
| |
| private final Context mContext; |
| private final String mDownloadLocationFrom; |
| private final Callback mCallback; |
| private final byte[] mHash; |
| private final String mHttpCookieHeader; |
| |
| private boolean mDoneDownloading; |
| private String mDownloadLocationTo; |
| private long mDownloadId; |
| private BroadcastReceiver mReceiver; |
| |
| public DownloadPackageTask (Context context, String downloadLocation, byte[] hash, |
| String httpCookieHeader, Callback callback) { |
| mCallback = callback; |
| mContext = context; |
| mDownloadLocationFrom = downloadLocation; |
| mHash = hash; |
| mHttpCookieHeader = httpCookieHeader; |
| mDoneDownloading = false; |
| } |
| |
| public boolean downloadLocationWasProvided() { |
| return !TextUtils.isEmpty(mDownloadLocationFrom); |
| } |
| |
| public void run() { |
| mReceiver = createDownloadReceiver(); |
| mContext.registerReceiver(mReceiver, |
| new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); |
| |
| ProvisionLogger.logd("Starting download from " + mDownloadLocationFrom); |
| DownloadManager dm = (DownloadManager) mContext |
| .getSystemService(Context.DOWNLOAD_SERVICE); |
| Request request = new Request(Uri.parse(mDownloadLocationFrom)); |
| if (mHttpCookieHeader != null) { |
| request.addRequestHeader("Cookie", mHttpCookieHeader); |
| ProvisionLogger.logd("Downloading with http cookie header: " + mHttpCookieHeader); |
| } |
| mDownloadId = dm.enqueue(request); |
| } |
| |
| private BroadcastReceiver createDownloadReceiver() { |
| return new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) { |
| Query q = new Query(); |
| q.setFilterById(mDownloadId); |
| DownloadManager dm = (DownloadManager) mContext |
| .getSystemService(Context.DOWNLOAD_SERVICE); |
| Cursor c = dm.query(q); |
| if (c.moveToFirst()) { |
| int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS); |
| if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) { |
| String location = c.getString( |
| c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME)); |
| c.close(); |
| onDownloadSuccess(location); |
| } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)){ |
| int reason = c.getColumnIndex(DownloadManager.COLUMN_REASON); |
| c.close(); |
| onDownloadFail(reason); |
| } |
| } |
| } |
| } |
| }; |
| } |
| |
| private void onDownloadSuccess(String location) { |
| if (mDoneDownloading) { |
| // DownloadManager can send success more than once. Only act first time. |
| return; |
| } else { |
| mDoneDownloading = true; |
| } |
| |
| ProvisionLogger.logd("Downloaded succesfully to: " + location); |
| |
| // Check whether hash of downloaded file matches hash given in constructor. |
| byte[] hash = computeHash(location); |
| if (hash == null) { |
| |
| // Error should have been reported in computeHash(). |
| return; |
| } |
| |
| if (Arrays.equals(mHash, hash)) { |
| ProvisionLogger.logd(HASH_TYPE + "-hashes matched, both are " |
| + byteArrayToString(hash)); |
| mDownloadLocationTo = location; |
| mCallback.onSuccess(); |
| } else { |
| ProvisionLogger.loge(HASH_TYPE + "-hash of downloaded file does not match given hash."); |
| ProvisionLogger.loge(HASH_TYPE + "-hash of downloaded file: " |
| + byteArrayToString(hash)); |
| ProvisionLogger.loge(HASH_TYPE + "-hash provided by programmer: " |
| + byteArrayToString(mHash)); |
| |
| mCallback.onError(ERROR_HASH_MISMATCH); |
| } |
| } |
| |
| 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[] computeHash(String fileLocation) { |
| InputStream fis = null; |
| MessageDigest md; |
| byte hash[] = null; |
| try { |
| md = MessageDigest.getInstance(HASH_TYPE); |
| } catch (NoSuchAlgorithmException e) { |
| ProvisionLogger.loge("Hashing algorithm " + HASH_TYPE + " 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() { |
| return mDownloadLocationTo; |
| } |
| |
| public void cleanUp() { |
| if (mReceiver != null) { |
| //Unregister receiver. |
| mContext.unregisterReceiver(mReceiver); |
| mReceiver = null; |
| } |
| |
| //Remove download. |
| DownloadManager dm = (DownloadManager) mContext |
| .getSystemService(Context.DOWNLOAD_SERVICE); |
| boolean removeSuccess = dm.remove(mDownloadId) == 1; |
| if (removeSuccess) { |
| ProvisionLogger.logd("Successfully removed the device owner installer file."); |
| } else { |
| ProvisionLogger.loge("Could not remove the device owner installer file."); |
| // Ignore this error. Failing cleanup should not stop provisioning flow. |
| } |
| } |
| |
| // For logging purposes only. |
| String byteArrayToString(byte[] ba) { |
| return Base64.encodeToString(ba, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); |
| } |
| |
| public abstract static class Callback { |
| public abstract void onSuccess(); |
| public abstract void onError(int errorCode); |
| } |
| } |