Revision 69ecaf02

View differences:

F-Droid/src/org/fdroid/fdroid/AppDetails.java
22 22
package org.fdroid.fdroid;
23 23

  
24 24
import android.app.Activity;
25
import android.app.DownloadManager;
26 25
import android.bluetooth.BluetoothAdapter;
27 26
import android.content.ActivityNotFoundException;
28 27
import android.content.BroadcastReceiver;
......
35 34
import android.content.pm.PackageManager;
36 35
import android.content.pm.Signature;
37 36
import android.database.ContentObserver;
38
import android.database.Cursor;
39 37
import android.graphics.Bitmap;
40 38
import android.net.Uri;
41 39
import android.os.Bundle;
......
94 92
import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException;
95 93
import org.fdroid.fdroid.installer.Installer.InstallerCallback;
96 94
import org.fdroid.fdroid.net.ApkDownloader;
97
import org.fdroid.fdroid.net.AsyncDownloader;
95
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
98 96
import org.fdroid.fdroid.net.Downloader;
99 97

  
100 98
import java.io.File;
......
435 433
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
436 434

  
437 435
        // Check if a download is running for this app
438
        if (AsyncDownloader.isDownloading(this, app.id) >= 0) {
436
        if (AsyncDownloaderFromAndroid.isDownloading(this, app.id) >= 0) {
439 437
            // call install() to re-setup the listeners and downloaders
440 438
            // the AsyncDownloader will not restart the download since the download is running,
441 439
            // and thus the version we pass to install() is not important
F-Droid/src/org/fdroid/fdroid/net/ApkDownloader.java
47 47
 * If the file has previously been downloaded, it will make use of that
48 48
 * instead, without going to the network to download a new one.
49 49
 */
50
public class ApkDownloader implements AsyncDownloadWrapper.Listener {
50
public class ApkDownloader implements AsyncDownloader.Listener {
51 51

  
52 52
    private static final String TAG = "ApkDownloader";
53 53

  
......
78 78
    @NonNull private final SanitizedFile potentiallyCachedFile;
79 79

  
80 80
    private ProgressListener listener;
81
    private AsyncDownloadWrapper dlWrapper = null;
81
    private AsyncDownloader dlWrapper = null;
82 82
    private boolean isComplete = false;
83 83

  
84 84
    private final long id = ++downloadIdCounter;
......
197 197
        Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile);
198 198

  
199 199
        try {
200
            if (canUseDownloadManager(new URL(remoteAddress))) {
201
                // If we can use Android's DownloadManager, let's use it, because
202
                // of better OS integration, reliability, and async ability
203
                dlWrapper = new AsyncDownloader(context, this,
204
                        app.name + " " + curApk.version, curApk.id,
205
                        remoteAddress, localFile);
206
            } else {
207
                Downloader downloader = DownloaderFactory.create(context, remoteAddress, localFile);
208
                dlWrapper = new AsyncDownloadWrapper(downloader, this);
209
            }
210

  
200
            dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this);
211 201
            dlWrapper.download();
212 202
            return true;
213 203
        } catch (IOException e) {
......
217 207
        return false;
218 208
    }
219 209

  
220
    /**
221
     * Tests to see if we can use Android's DownloadManager to download the APK, instead of
222
     * a downloader returned from DownloadFactory.
223
     * @param url
224
     * @return
225
     */
226
    private boolean canUseDownloadManager(URL url) {
227
        return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO
228
                && !DownloaderFactory.isOnionAddress(url);
229
    }
230

  
231 210
    private void sendMessage(String type) {
232 211
        sendProgressEvent(new ProgressListener.Event(type));
233 212
    }
F-Droid/src/org/fdroid/fdroid/net/AsyncDownloadWrapper.java
1
package org.fdroid.fdroid.net;
2

  
3
import android.os.Bundle;
4
import android.os.Handler;
5
import android.os.Message;
6
import android.util.Log;
7

  
8
import org.fdroid.fdroid.ProgressListener;
9

  
10
import java.io.IOException;
11

  
12
/**
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 AsyncDownloadWrapper#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.
18
 */
19
@SuppressWarnings("serial")
20
public class AsyncDownloadWrapper extends Handler {
21

  
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;
32

  
33
    /**
34
     * Normally the listener would be provided using a setListener method.
35
     * However for the purposes of this async downloader, it doesn't make
36
     * sense to have an async task without any way to notify the outside
37
     * world about completion. Therefore, we require the listener as a
38
     * parameter to the constructor.
39
     */
40
    public AsyncDownloadWrapper(Downloader downloader, Listener listener) {
41
        this.downloader = downloader;
42
        this.listener   = listener;
43
    }
44

  
45
    public void download() {
46
        downloadThread = new DownloadThread();
47
        downloadThread.start();
48
    }
49

  
50
    public void attemptCancel(boolean userRequested) {
51
        if (downloadThread != null) {
52
            downloadThread.interrupt();
53
        }
54
    }
55

  
56
    /**
57
     * Receives "messages" from the download thread, and passes them onto the
58
     * relevant {@link org.fdroid.fdroid.net.AsyncDownloadWrapper.Listener}
59
     * @param message
60
     */
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;
72
        }
73
    }
74

  
75
    public int getBytesRead() {
76
        return downloader.getBytesRead();
77
    }
78

  
79
    public int getTotalBytes() {
80
        return downloader.getTotalBytes();
81
    }
82

  
83
    public interface Listener extends ProgressListener {
84
        void onErrorDownloading(String localisedExceptionDetails);
85
        void onDownloadComplete();
86
        void onDownloadCancelled();
87
    }
88

  
89
    private class DownloadThread extends Thread {
90

  
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
                AsyncDownloadWrapper.this.sendMessage(message);
105
            }
106
        }
107

  
108
        private void sendMessage(int messageType) {
109
            Message message = new Message();
110
            message.arg1 = messageType;
111
            AsyncDownloadWrapper.this.sendMessage(message);
112
        }
113
    }
114
}
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
}
F-Droid/src/org/fdroid/fdroid/net/AsyncDownloaderFromAndroid.java
1
package org.fdroid.fdroid.net;
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

  
14
import java.io.File;
15
import java.io.FileDescriptor;
16
import java.io.FileInputStream;
17
import java.io.FileOutputStream;
18
import java.io.IOException;
19
import java.io.InputStream;
20
import java.io.OutputStream;
21

  
22
/**
23
 * A downloader that uses Android's DownloadManager to perform a download.
24
 */
25
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
26
public class AsyncDownloaderFromAndroid extends AsyncDownloader {
27
    private final Context context;
28
    private final DownloadManager dm;
29
    private File localFile;
30
    private String remoteAddress;
31
    private String appName;
32
    private String appId;
33
    private Listener listener;
34

  
35
    private long downloadId = -1;
36

  
37
    /**
38
     * Normally the listener would be provided using a setListener method.
39
     * However for the purposes of this async downloader, it doesn't make
40
     * sense to have an async task without any way to notify the outside
41
     * world about completion. Therefore, we require the listener as a
42
     * parameter to the constructor.
43
     *
44
     * @param listener
45
     */
46
    public AsyncDownloaderFromAndroid(Context context, Listener listener, String appName, String appId, String remoteAddress, File localFile) {
47
        super(null, listener);
48
        this.context = context;
49
        this.appName = appName;
50
        this.appId = appId;
51
        this.remoteAddress = remoteAddress;
52
        this.listener = listener;
53
        this.localFile = localFile;
54

  
55
        if (appName == null || appName.trim().length() == 0) {
56
            this.appName = remoteAddress;
57
        }
58

  
59
        dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
60
    }
61

  
62
    @Override
63
    public void download() {
64
        // Check if the download is complete
65
        if ((downloadId = isDownloadComplete(context, appId)) > 0) {
66
            // clear the notification
67
            dm.remove(downloadId);
68

  
69
            try {
70
                // write the downloaded file to the expected location
71
                ParcelFileDescriptor fd = dm.openDownloadedFile(downloadId);
72
                copyFile(fd.getFileDescriptor(), localFile);
73
                listener.onDownloadComplete();
74
            } catch (IOException e) {
75
                listener.onErrorDownloading(e.getLocalizedMessage());
76
            }
77
            return;
78
        }
79

  
80
        // Check if the download is still in progress
81
        if (downloadId < 0) {
82
            downloadId = isDownloading(context, appId);
83
        }
84

  
85
        // Start a new download
86
        if (downloadId < 0) {
87
            // set up download request
88
            DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteAddress));
89
            request.setTitle(appName);
90
            request.setDescription(appId); // we will retrieve this later from the description field
91
            this.downloadId = dm.enqueue(request);
92
        }
93

  
94
        context.registerReceiver(receiver,
95
                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
96
    }
97

  
98
    /**
99
     * Copy input file to output file
100
     * @param inputFile
101
     * @param outputFile
102
     * @throws IOException
103
     */
104
    private void copyFile(FileDescriptor inputFile, File outputFile) throws IOException {
105
        InputStream is = new FileInputStream(inputFile);
106
        OutputStream os = new FileOutputStream(outputFile);
107
        byte[] buffer = new byte[1024];
108
        int count = 0;
109

  
110
        try {
111
            while ((count = is.read(buffer, 0, buffer.length)) > 0) {
112
                os.write(buffer, 0, count);
113
            }
114
        } finally {
115
            os.close();
116
            is.close();
117
        }
118
    }
119

  
120
    @Override
121
    public int getBytesRead() {
122
        if (downloadId < 0) return 0;
123

  
124
        DownloadManager.Query query = new DownloadManager.Query();
125
        query.setFilterById(downloadId);
126
        Cursor c = dm.query(query);
127

  
128
        try {
129
            if (c.moveToFirst()) {
130
                // we use the description column to store the app id
131
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
132
                return c.getInt(columnIndex);
133
            }
134
        } finally {
135
            c.close();
136
        }
137

  
138
        return 0;
139
    }
140

  
141
    @Override
142
    public int getTotalBytes() {
143
        if (downloadId < 0) return 0;
144

  
145
        DownloadManager.Query query = new DownloadManager.Query();
146
        query.setFilterById(downloadId);
147
        Cursor c = dm.query(query);
148

  
149
        try {
150
            if (c.moveToFirst()) {
151
                // we use the description column to store the app id
152
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
153
                return c.getInt(columnIndex);
154
            }
155
        } finally {
156
            c.close();
157
        }
158

  
159
        return 0;
160
    }
161

  
162
    @Override
163
    public void attemptCancel(boolean userRequested) {
164
        try {
165
            context.unregisterReceiver(receiver);
166
        } catch (Exception e) {
167
            // ignore if receiver already unregistered
168
        }
169

  
170
        if (userRequested && downloadId >= 0) {
171
            dm.remove(downloadId);
172
        }
173
    }
174

  
175
    /**
176
     * Extract the appId from a given download id.
177
     * @param context
178
     * @param downloadId
179
     * @return - appId or null if not found
180
     */
181
    public static String getAppId(Context context, long downloadId) {
182
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
183
        DownloadManager.Query query = new DownloadManager.Query();
184
        query.setFilterById(downloadId);
185
        Cursor c = dm.query(query);
186

  
187
        try {
188
            if (c.moveToFirst()) {
189
                // we use the description column to store the app id
190
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
191
                return c.getString(columnIndex);
192
            }
193
        } finally {
194
            c.close();
195
        }
196

  
197
        return null;
198
    }
199

  
200
    /**
201
     * Extract the download title from a given download id.
202
     * @param context
203
     * @param downloadId
204
     * @return - title or null if not found
205
     */
206
    public static String getDownloadTitle(Context context, long downloadId) {
207
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
208
        DownloadManager.Query query = new DownloadManager.Query();
209
        query.setFilterById(downloadId);
210
        Cursor c = dm.query(query);
211

  
212
        try {
213
            if (c.moveToFirst()) {
214
                // we use the description column to store the app id
215
                int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_TITLE);
216
                return c.getString(columnIndex);
217
            }
218
        } finally {
219
            c.close();
220
        }
221

  
222
        return null;
223
    }
224

  
225
    /**
226
     * Get the downloadId from an Intent sent by the DownloadManagerReceiver
227
     * @param intent
228
     * @return
229
     */
230
    public static long getDownloadId(Intent intent) {
231
        if (intent != null) {
232
            if (intent.hasExtra(DownloadManager.EXTRA_DOWNLOAD_ID)) {
233
                // we have been passed a DownloadManager download id, so get the app id for it
234
                return intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
235
            }
236

  
237
            if (intent.hasExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS)) {
238
                // we have been passed multiple download id's - just return the first one
239
                long[] downloadIds = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
240
                if (downloadIds != null && downloadIds.length > 0) {
241
                    return downloadIds[0];
242
                }
243
            }
244
        }
245

  
246
        return -1;
247
    }
248

  
249
    /**
250
     * Check if a download is running for the app
251
     * @param context
252
     * @param appId
253
     * @return -1 if not downloading, else the downloadId
254
     */
255
    public static long isDownloading(Context context, String appId) {
256
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
257
        DownloadManager.Query query = new DownloadManager.Query();
258
        Cursor c = dm.query(query);
259
        int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
260
        int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
261

  
262
        try {
263
            while (c.moveToNext()) {
264
                if (appId.equals(c.getString(columnAppId))) {
265
                    return c.getLong(columnId);
266
                }
267
            }
268
        } finally {
269
            c.close();
270
        }
271

  
272
        return -1;
273
    }
274

  
275
    /**
276
     * Check if a download for an app is complete.
277
     * @param context
278
     * @param appId
279
     * @return -1 if download is not complete, otherwise the download id
280
     */
281
    public static long isDownloadComplete(Context context, String appId) {
282
        DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
283
        DownloadManager.Query query = new DownloadManager.Query();
284
        query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
285
        Cursor c = dm.query(query);
286
        int columnAppId = c.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
287
        int columnId = c.getColumnIndex(DownloadManager.COLUMN_ID);
288

  
289
        try {
290
            while (c.moveToNext()) {
291
                if (appId.equals(c.getString(columnAppId))) {
292
                    return c.getLong(columnId);
293
                }
294
            }
295
        } finally {
296
            c.close();
297
        }
298

  
299
        return -1;
300
    }
301

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

  
320
                    // call download() to copy the file and start the installer
321
                    download();
322
                }
323
            }
324
        }
325
    };
326
}
F-Droid/src/org/fdroid/fdroid/net/DownloaderFactory.java
1 1
package org.fdroid.fdroid.net;
2 2

  
3 3
import android.content.Context;
4
import android.os.Build;
4 5

  
5 6
import java.io.File;
6 7
import java.io.IOException;
8
import java.net.MalformedURLException;
7 9
import java.net.URL;
8 10

  
9 11
public class DownloaderFactory {
......
51 53
        return "bluetooth".equalsIgnoreCase(url.getProtocol());
52 54
    }
53 55

  
56
    public static AsyncDownloader createAsync(Context context, String urlString, File destFile, String title, String id, AsyncDownloader.Listener listener) throws IOException {
57
        return createAsync(context, new URL(urlString), destFile, title, id, listener);
58
    }
59

  
60
    public static AsyncDownloader createAsync(Context context, URL url, File destFile, String title, String id, AsyncDownloader.Listener listener)
61
            throws IOException {
62
        if (canUseDownloadManager(url)) {
63
            return new AsyncDownloaderFromAndroid(context, listener, title, id, url.toString(), destFile);
64
        } else {
65
            return new AsyncDownloader(create(context, url, destFile), listener);
66
        }
67
    }
68

  
54 69
    static boolean isOnionAddress(URL url) {
55 70
        return url.getHost().endsWith(".onion");
56 71
    }
72

  
73
    /**
74
     * Tests to see if we can use Android's DownloadManager to download the APK, instead of
75
     * a downloader returned from DownloadFactory.
76
     */
77
    private static boolean canUseDownloadManager(URL url) {
78
        return Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO && !isOnionAddress(url);
79
    }
80

  
57 81
}
F-Droid/src/org/fdroid/fdroid/receiver/DownloadManagerReceiver.java
11 11

  
12 12
import org.fdroid.fdroid.AppDetails;
13 13
import org.fdroid.fdroid.R;
14
import org.fdroid.fdroid.net.AsyncDownloader;
14
import org.fdroid.fdroid.net.AsyncDownloaderFromAndroid;
15 15

  
16 16
/**
17 17
 * Receive notifications from the Android DownloadManager and pass them onto the
......
21 21
    @Override
22 22
    public void onReceive(Context context, Intent intent) {
23 23
        // work out the app Id to send to the AppDetails Screen
24
        long downloadId = AsyncDownloader.getDownloadId(intent);
25
        String appId = AsyncDownloader.getAppId(context, downloadId);
24
        long downloadId = AsyncDownloaderFromAndroid.getDownloadId(intent);
25
        String appId = AsyncDownloaderFromAndroid.getAppId(context, downloadId);
26 26

  
27 27
        if (appId == null) {
28 28
            // bogus broadcast (e.g. download cancelled, but system sent a DOWNLOAD_COMPLETE)
......
41 41
                    context, 1, appDetails, PendingIntent.FLAG_ONE_SHOT);
42 42

  
43 43
            // launch LocalRepoActivity if the user selects this notification
44
            String downloadTitle = AsyncDownloader.getDownloadTitle(context, downloadId);
44
            String downloadTitle = AsyncDownloaderFromAndroid.getDownloadTitle(context, downloadId);
45 45
            Notification notif = new NotificationCompat.Builder(context)
46 46
                    .setContentTitle(downloadTitle)
47 47
                    .setContentText(context.getString(R.string.tap_to_install))

Also available in: Unified diff