Revision 69ecaf02 F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java

View differences:

F-Droid/src/org/fdroid/fdroid/net/AsyncDownloader.java
1 1
package org.fdroid.fdroid.net;
2 2

  
3
import android.annotation.TargetApi;
4
import android.app.DownloadManager;
5
import android.content.BroadcastReceiver;
6
import android.content.Context;
7
import android.content.Intent;
8
import android.content.IntentFilter;
9
import android.database.Cursor;
10
import android.net.Uri;
11
import android.os.Build;
12
import android.os.ParcelFileDescriptor;
13
import android.support.v4.content.LocalBroadcastManager;
3
import android.os.Bundle;
4
import android.os.Handler;
5
import android.os.Message;
14 6
import android.util.Log;
15 7

  
16
import org.fdroid.fdroid.AppDetails;
17 8
import org.fdroid.fdroid.ProgressListener;
18
import org.fdroid.fdroid.data.Apk;
19
import org.fdroid.fdroid.data.App;
20
import org.fdroid.fdroid.data.SanitizedFile;
21 9

  
22
import java.io.ByteArrayOutputStream;
23
import java.io.File;
24
import java.io.FileDescriptor;
25
import java.io.FileInputStream;
26
import java.io.FileOutputStream;
27 10
import java.io.IOException;
28
import java.io.InputStream;
29
import java.io.OutputStream;
30 11

  
31 12
/**
32
 * A downloader that uses Android's DownloadManager to perform a download.
13
 * Given a {@link org.fdroid.fdroid.net.Downloader}, this wrapper will conduct the download operation on a
14
 * separate thread. All progress/status/error/etc events will be forwarded from that thread to the thread
15
 * that {@link AsyncDownloader#download()} was invoked on. If you want to respond with UI feedback
16
 * to these events, it is important that you execute the download method of this class from the UI thread.
17
 * That way, all forwarded events will be handled on that thread.
33 18
 */
34
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
35
public class AsyncDownloader extends AsyncDownloadWrapper {
36
    private final Context context;
37
    private final DownloadManager dm;
38
    private SanitizedFile localFile;
39
    private String remoteAddress;
40
    private String appName;
41
    private String appId;
42
    private Listener listener;
19
@SuppressWarnings("serial")
20
public class AsyncDownloader extends Handler {
43 21

  
44
    private long downloadId = -1;
22
    private static final String TAG = "AsyncDownloadWrapper";
23

  
24
    private static final int MSG_DOWNLOAD_COMPLETE  = 2;
25
    private static final int MSG_DOWNLOAD_CANCELLED = 3;
26
    private static final int MSG_ERROR              = 4;
27
    private static final String MSG_DATA            = "data";
28

  
29
    private final Downloader downloader;
30
    private final Listener listener;
31
    private DownloadThread downloadThread = null;
45 32

  
46 33
    /**
47 34
     * Normally the listener would be provided using a setListener method.
......
49 36
     * sense to have an async task without any way to notify the outside
50 37
     * world about completion. Therefore, we require the listener as a
51 38
     * parameter to the constructor.
52
     *
53
     * @param listener
54 39
     */
55
    public AsyncDownloader(Context context, Listener listener, String appName, String appId, String remoteAddress, SanitizedFile localFile) {
56
        super(null, listener);
57
        this.context = context;
58
        this.appName = appName;
59
        this.appId = appId;
60
        this.remoteAddress = remoteAddress;
61
        this.listener = listener;
62
        this.localFile = localFile;
63

  
64
        if (appName == null || appName.trim().length() == 0) {
65
            this.appName = remoteAddress;
66
        }
67

  
68
        dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
40
    public AsyncDownloader(Downloader downloader, Listener listener) {
41
        this.downloader = downloader;
42
        this.listener   = listener;
69 43
    }
70 44

  
71
    @Override
72 45
    public void download() {
73
        // Check if the download is complete
74
        if ((downloadId = isDownloadComplete(context, appId)) > 0) {
75
            // clear the notification
76
            dm.remove(downloadId);
77

  
78
            try {
79
                // write the downloaded file to the expected location
80
                ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId);
81
                copyFile(fd.getFileDescriptor(), localFile);
82
                listener.onDownloadComplete();
83
            } catch (IOException e) {
84
                listener.onErrorDownloading(e.getLocalizedMessage());
85
            }
86
            return;
87
        }
88

  
89
        // Check if the download is still in progress
90
        if (downloadId < 0) {
91
            downloadId = isDownloading(context, appId);
92
        }
46
        downloadThread = new DownloadThread();
47
        downloadThread.start();
48
    }
93 49

  
94
        // Start a new download
95
        if (downloadId < 0) {
96
            // set up download request
97
            DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress));
98
            request.setTitle(appName);
99
            request.setDescription(appId); // we will retrieve this later from the description field
100
            this.downloadId = dm.enqueue(request);
50
    public void attemptCancel(boolean userRequested) {
51
        if (downloadThread != null) {
52
            downloadThread.interrupt();
101 53
        }
102

  
103
        context.registerReceiver(receiver,
104
                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
105 54
    }
106 55

  
107 56
    /**
108
     * Copy input file to output file
109
     * @param inputFile
110
     * @param outputFile
111
     * @throws IOException
57
     * Receives "messages" from the download thread, and passes them onto the
58
     * relevant {@link AsyncDownloader.Listener}
59
     * @param message
112 60
     */
113
    private void copyFile(FileDescriptor inputFile, SanitizedFile outputFile) throws IOException {
114
        InputStream is = new FileInputStream(inputFile);
115
        OutputStream os = new FileOutputStream(outputFile);
116
        byte[] buffer = new byte[1024];
117
        int count = 0;
118

  
119
        try {
120
            while ((count = is.read(buffer, 0, buffer.length)) > 0) {
121
                os.write(buffer, 0, count);
122
            }
123
        } finally {
124
            os.close();
125
            is.close();
61
    public void handleMessage(Message message) {
62
        switch (message.arg1) {
63
        case MSG_DOWNLOAD_COMPLETE:
64
            listener.onDownloadComplete();
65
            break;
66
        case MSG_DOWNLOAD_CANCELLED:
67
            listener.onDownloadCancelled();
68
            break;
69
        case MSG_ERROR:
70
            listener.onErrorDownloading(message.getData().getString(MSG_DATA));
71
            break;
126 72
        }
127 73
    }
128 74

  
129
    @Override
130 75
    public int getBytesRead() {
131
        if (downloadId < 0) return 0;
132

  
133
        DownloadManager.Query query = new DownloadManager.Query();
134
        query.setFilterById(downloadId);
135
        Cursor c = dm.query(query);
136

  
137
        try {
138
            if (c.moveToFirst()) {
139
                // we use the description column to store the app id
140
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
141
                return c.getInt(columnIndex);
142
            }
143
        } finally {
144
            c.close();
145
        }
146

  
147
        return 0;
76
        return downloader.getBytesRead();
148 77
    }
149 78

  
150
    @Override
151 79
    public int getTotalBytes() {
152
        if (downloadId < 0) return 0;
153

  
154
        DownloadManager.Query query = new DownloadManager.Query();
155
        query.setFilterById(downloadId);
156
        Cursor c = dm.query(query);
157

  
158
        try {
159
            if (c.moveToFirst()) {
160
                // we use the description column to store the app id
161
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
162
                return c.getInt(columnIndex);
163
            }
164
        } finally {
165
            c.close();
166
        }
167

  
168
        return 0;
80
        return downloader.getTotalBytes();
169 81
    }
170 82

  
171
    @Override
172
    public void attemptCancel(boolean userRequested) {
173
        try {
174
            context.unregisterReceiver(receiver);
175
        } catch (Exception e) {
176
            // ignore if receiver already unregistered
177
        }
178

  
179
        if (userRequested && downloadId >= 0) {
180
            dm.remove(downloadId);
181
        }
83
    public interface Listener extends ProgressListener {
84
        void onErrorDownloading(String localisedExceptionDetails);
85
        void onDownloadComplete();
86
        void onDownloadCancelled();
182 87
    }
183 88

  
184
    /**
185
     * Extract the appId from a given download id.
186
     * @param context
187
     * @param downloadId
188
     * @return - appId or null if not found
189
     */
190
    public static String getAppId(Context context, long downloadId) {
191
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
192
        DownloadManager.Query query = new DownloadManager.Query();
193
        query.setFilterById(downloadId);
194
        Cursor c = dm.query(query);
89
    private class DownloadThread extends Thread {
195 90

  
196
        try {
197
            if (c.moveToFirst()) {
198
                // we use the description column to store the app id
199
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
200
                return c.getString(columnIndex);
201
            }
202
        } finally {
203
            c.close();
204
        }
205

  
206
        return null;
207
    }
208

  
209
    /**
210
     * Extract the download title from a given download id.
211
     * @param context
212
     * @param downloadId
213
     * @return - title or null if not found
214
     */
215
    public static String getDownloadTitle(Context context, long downloadId) {
216
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
217
        DownloadManager.Query query = new DownloadManager.Query();
218
        query.setFilterById(downloadId);
219
        Cursor c = dm.query(query);
220

  
221
        try {
222
            if (c.moveToFirst()) {
223
                // we use the description column to store the app id
224
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE);
225
                return c.getString(columnIndex);
226
            }
227
        } finally {
228
            c.close();
229
        }
230

  
231
        return null;
232
    }
233

  
234
    /**
235
     * Get the downloadId from an Intent sent by the DownloadManagerReceiver
236
     * @param intent
237
     * @return
238
     */
239
    public static long getDownloadId(Intent intent) {
240
        if (intent != null) {
241
            if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) {
242
                // we have been passed a DownloadManager download id, so get the app id for it
243
                return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
244
            }
245

  
246
            if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) {
247
                // we have been passed multiple download id's - just return the first one
248
                long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
249
                if (downloadIds != null && downloadIds.length > 0) {
250
                    return downloadIds[0];
251
                }
252
            }
253
        }
254

  
255
        return -1;
256
    }
257

  
258
    /**
259
     * Check if a download is running for the app
260
     * @param context
261
     * @param appId
262
     * @return -1 if not downloading, else the downloadId
263
     */
264
    public static long isDownloading(Context context, String appId) {
265
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
266
        DownloadManager.Query query = new DownloadManager.Query();
267
        Cursor c = dm.query(query);
268
        int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
269
        int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
270

  
271
        try {
272
            while (c.moveToNext()) {
273
                if (appId.equals(c.getString(columnAppId))) {
274
                    return c.getLong(columnId);
275
                }
91
        public void run() {
92
            try {
93
                downloader.download();
94
                sendMessage(MSG_DOWNLOAD_COMPLETE);
95
            } catch (InterruptedException e) {
96
                sendMessage(MSG_DOWNLOAD_CANCELLED);
97
            } catch (IOException e) {
98
                Log.e(TAG, "I/O exception in download thread", e);
99
                Bundle data = new Bundle(1);
100
                data.putString(MSG_DATA, e.getLocalizedMessage());
101
                Message message = new Message();
102
                message.arg1 = MSG_ERROR;
103
                message.setData(data);
104
                AsyncDownloader.this.sendMessage(message);
276 105
            }
277
        } finally {
278
            c.close();
279 106
        }
280 107

  
281
        return -1;
282
    }
283

  
284
    /**
285
     * Check if a download for an app is complete.
286
     * @param context
287
     * @param appId
288
     * @return -1 if download is not complete, otherwise the download id
289
     */
290
    public static long isDownloadComplete(Context context, String appId) {
291
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
292
        DownloadManager.Query query = new DownloadManager.Query();
293
        query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
294
        Cursor c = dm.query(query);
295
        int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
296
        int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
297

  
298
        try {
299
            while (c.moveToNext()) {
300
                if (appId.equals(c.getString(columnAppId))) {
301
                    return c.getLong(columnId);
302
                }
303
            }
304
        } finally {
305
            c.close();
108
        private void sendMessage(int messageType) {
109
            Message message = new Message();
110
            message.arg1 = messageType;
111
            AsyncDownloader.this.sendMessage(message);
306 112
        }
307

  
308
        return -1;
309 113
    }
310

  
311
    /**
312
     * Broadcast receiver to listen for ACTION_DOWNLOAD_COMPLETE broadcasts
313
     */
314
    BroadcastReceiver receiver = new BroadcastReceiver() {
315
        @Override
316
        public void onReceive(Context context, Intent intent) {
317
            if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
318
                long dId = getDownloadId(intent);
319
                String appId = getAppId(context, dId);
320
                if (listener != null && dId == downloadId && appId != null) {
321
                    // our current download has just completed, so let's throw up install dialog
322
                    // immediately
323
                    try {
324
                        context.unregisterReceiver(receiver);
325
                    } catch (Exception e) {
326
                        // ignore if receiver already unregistered
327
                    }
328

  
329
                    // call download() to copy the file and start the installer
330
                    download();
331
                }
332
            }
333
        }
334
    };
335 114
}

Also available in: Unified diff