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