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.ActivityInfo;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager;
28import android.database.Cursor;
29import android.net.Uri;
30import android.text.TextUtils;
31import android.util.Base64;
32
33import com.android.managedprovisioning.ProvisionLogger;
34
35import java.io.InputStream;
36import java.io.IOException;
37import java.io.FileInputStream;
38import java.security.MessageDigest;
39import java.security.NoSuchAlgorithmException;
40import java.util.Arrays;
41
42/**
43 * Downloads a given file and checks whether its hash matches a given hash to verify that the
44 * intended file was downloaded.
45 */
46public class DownloadPackageTask {
47    public static final int ERROR_HASH_MISMATCH = 0;
48    public static final int ERROR_DOWNLOAD_FAILED = 1;
49    public static final int ERROR_OTHER = 2;
50
51    private static final String HASH_TYPE = "SHA-1";
52
53    private final Context mContext;
54    private final String mDownloadLocationFrom;
55    private final Callback mCallback;
56    private final byte[] mHash;
57    private final String mHttpCookieHeader;
58
59    private boolean mDoneDownloading;
60    private String mDownloadLocationTo;
61    private long mDownloadId;
62    private BroadcastReceiver mReceiver;
63
64    public DownloadPackageTask (Context context, String downloadLocation, byte[] hash,
65            String httpCookieHeader, Callback callback) {
66        mCallback = callback;
67        mContext = context;
68        mDownloadLocationFrom = downloadLocation;
69        mHash = hash;
70        mHttpCookieHeader = httpCookieHeader;
71        mDoneDownloading = false;
72    }
73
74    public boolean downloadLocationWasProvided() {
75        return !TextUtils.isEmpty(mDownloadLocationFrom);
76    }
77
78    public void run() {
79        mReceiver = createDownloadReceiver();
80        mContext.registerReceiver(mReceiver,
81                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
82
83        ProvisionLogger.logd("Starting download from " + mDownloadLocationFrom);
84        DownloadManager dm = (DownloadManager) mContext
85                .getSystemService(Context.DOWNLOAD_SERVICE);
86        Request request = new Request(Uri.parse(mDownloadLocationFrom));
87        if (mHttpCookieHeader != null) {
88            request.addRequestHeader("Cookie", mHttpCookieHeader);
89            ProvisionLogger.logd("Downloading with http cookie header: " + mHttpCookieHeader);
90        }
91        mDownloadId = dm.enqueue(request);
92    }
93
94    private BroadcastReceiver createDownloadReceiver() {
95        return new BroadcastReceiver() {
96            @Override
97            public void onReceive(Context context, Intent intent) {
98                if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
99                    Query q = new Query();
100                    q.setFilterById(mDownloadId);
101                    DownloadManager dm = (DownloadManager) mContext
102                            .getSystemService(Context.DOWNLOAD_SERVICE);
103                    Cursor c = dm.query(q);
104                    if (c.moveToFirst()) {
105                        int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
106                        if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
107                            String location = c.getString(
108                                    c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
109                            c.close();
110                            onDownloadSuccess(location);
111                        } else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)){
112                            int reason = c.getColumnIndex(DownloadManager.COLUMN_REASON);
113                            c.close();
114                            onDownloadFail(reason);
115                        }
116                    }
117                }
118            }
119        };
120    }
121
122    private void onDownloadSuccess(String location) {
123        if (mDoneDownloading) {
124            // DownloadManager can send success more than once. Only act first time.
125            return;
126        } else {
127            mDoneDownloading = true;
128        }
129
130        ProvisionLogger.logd("Downloaded succesfully to: " + location);
131
132        // Check whether hash of downloaded file matches hash given in constructor.
133        byte[] hash = computeHash(location);
134        if (hash == null) {
135
136            // Error should have been reported in computeHash().
137            return;
138        }
139
140        if (Arrays.equals(mHash, hash)) {
141            ProvisionLogger.logd(HASH_TYPE + "-hashes matched, both are "
142                    + byteArrayToString(hash));
143            mDownloadLocationTo = location;
144            mCallback.onSuccess();
145        } else {
146            ProvisionLogger.loge(HASH_TYPE + "-hash of downloaded file does not match given hash.");
147            ProvisionLogger.loge(HASH_TYPE + "-hash of downloaded file: "
148                    + byteArrayToString(hash));
149            ProvisionLogger.loge(HASH_TYPE + "-hash provided by programmer: "
150                    + byteArrayToString(mHash));
151
152            mCallback.onError(ERROR_HASH_MISMATCH);
153        }
154    }
155
156    private void onDownloadFail(int errorCode) {
157        ProvisionLogger.loge("Downloading package failed.");
158        ProvisionLogger.loge("COLUMN_REASON in DownloadManager response has value: "
159                + errorCode);
160        mCallback.onError(ERROR_DOWNLOAD_FAILED);
161    }
162
163    private byte[] computeHash(String fileLocation) {
164        InputStream fis = null;
165        MessageDigest md;
166        byte hash[] = null;
167        try {
168            md = MessageDigest.getInstance(HASH_TYPE);
169        } catch (NoSuchAlgorithmException e) {
170            ProvisionLogger.loge("Hashing algorithm " + HASH_TYPE + " not supported.", e);
171            mCallback.onError(ERROR_OTHER);
172            return null;
173        }
174        try {
175            fis = new FileInputStream(fileLocation);
176
177            byte[] buffer = new byte[256];
178            int n = 0;
179            while (n != -1) {
180                n = fis.read(buffer);
181                if (n > 0) {
182                    md.update(buffer, 0, n);
183                }
184            }
185            hash = md.digest();
186        } catch (IOException e) {
187            ProvisionLogger.loge("IO error.", e);
188            mCallback.onError(ERROR_OTHER);
189        } finally {
190            // Close input stream quietly.
191            try {
192                if (fis != null) {
193                    fis.close();
194                }
195            } catch (IOException e) {
196                // Ignore.
197            }
198        }
199        return hash;
200    }
201
202    public String getDownloadedPackageLocation() {
203        return mDownloadLocationTo;
204    }
205
206    public void cleanUp() {
207        if (mReceiver != null) {
208            //Unregister receiver.
209            mContext.unregisterReceiver(mReceiver);
210            mReceiver = null;
211        }
212
213        //Remove download.
214        DownloadManager dm = (DownloadManager) mContext
215                .getSystemService(Context.DOWNLOAD_SERVICE);
216        boolean removeSuccess = dm.remove(mDownloadId) == 1;
217        if (removeSuccess) {
218            ProvisionLogger.logd("Successfully removed the device owner installer file.");
219        } else {
220            ProvisionLogger.loge("Could not remove the device owner installer file.");
221            // Ignore this error. Failing cleanup should not stop provisioning flow.
222        }
223    }
224
225    // For logging purposes only.
226    String byteArrayToString(byte[] ba) {
227        return Base64.encodeToString(ba, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
228    }
229
230    public abstract static class Callback {
231        public abstract void onSuccess();
232        public abstract void onError(int errorCode);
233    }
234}
235