1/*
2 * Copyright (C) 2010 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 */
16
17package com.android.frameworks.downloadmanagertests;
18
19import android.app.DownloadManager;
20import android.app.DownloadManager.Query;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.database.Cursor;
26import android.net.ConnectivityManager;
27import android.net.NetworkInfo;
28import android.net.wifi.WifiManager;
29import android.os.Environment;
30import android.os.ParcelFileDescriptor;
31import android.os.SystemClock;
32import android.provider.Settings;
33import android.test.InstrumentationTestCase;
34import android.util.Log;
35
36import java.io.File;
37import java.util.Collections;
38import java.util.HashSet;
39import java.util.Set;
40import java.util.concurrent.TimeoutException;
41
42/**
43 * Base class for Instrumented tests for the Download Manager.
44 */
45public class DownloadManagerBaseTest extends InstrumentationTestCase {
46
47    protected DownloadManager mDownloadManager = null;
48    protected String mFileType = "text/plain";
49    protected Context mContext = null;
50    protected MultipleDownloadsCompletedReceiver mReceiver = null;
51    protected static final int DEFAULT_FILE_SIZE = 10 * 1024;  // 10kb
52    protected static final int FILE_BLOCK_READ_SIZE = 1024 * 1024;
53
54    protected static final String LOG_TAG = "android.net.DownloadManagerBaseTest";
55    protected static final int HTTP_OK = 200;
56    protected static final int HTTP_REDIRECT = 307;
57    protected static final int HTTP_PARTIAL_CONTENT = 206;
58    protected static final int HTTP_NOT_FOUND = 404;
59    protected static final int HTTP_SERVICE_UNAVAILABLE = 503;
60
61    protected static final int DEFAULT_MAX_WAIT_TIME = 2 * 60 * 1000;  // 2 minutes
62    protected static final int DEFAULT_WAIT_POLL_TIME = 5 * 1000;  // 5 seconds
63
64    protected static final int WAIT_FOR_DOWNLOAD_POLL_TIME = 1 * 1000;  // 1 second
65    protected static final int MAX_WAIT_FOR_DOWNLOAD_TIME = 5 * 60 * 1000; // 5 minutes
66    protected static final int MAX_WAIT_FOR_LARGE_DOWNLOAD_TIME = 15 * 60 * 1000; // 15 minutes
67
68    public static class MultipleDownloadsCompletedReceiver extends BroadcastReceiver {
69        private volatile int mNumDownloadsCompleted = 0;
70        private Set<Long> downloadIds = Collections.synchronizedSet(new HashSet<Long>());
71
72        /**
73         * {@inheritDoc}
74         */
75        @Override
76        public void onReceive(Context context, Intent intent) {
77            if (intent.getAction().equalsIgnoreCase(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
78                synchronized(this) {
79                    long id = intent.getExtras().getLong(DownloadManager.EXTRA_DOWNLOAD_ID);
80                    Log.i(LOG_TAG, "Received Notification for download: " + id);
81                    if (!downloadIds.contains(id)) {
82                        ++mNumDownloadsCompleted;
83                        Log.i(LOG_TAG, "MultipleDownloadsCompletedReceiver got intent: " +
84                                intent.getAction() + " --> total count: " + mNumDownloadsCompleted);
85                        downloadIds.add(id);
86
87                        DownloadManager dm = (DownloadManager)context.getSystemService(
88                                Context.DOWNLOAD_SERVICE);
89
90                        Cursor cursor = dm.query(new Query().setFilterById(id));
91                        try {
92                            if (cursor.moveToFirst()) {
93                                int status = cursor.getInt(cursor.getColumnIndex(
94                                        DownloadManager.COLUMN_STATUS));
95                                Log.i(LOG_TAG, "Download status is: " + status);
96                            } else {
97                                fail("No status found for completed download!");
98                            }
99                        } finally {
100                            cursor.close();
101                        }
102                    } else {
103                        Log.i(LOG_TAG, "Notification for id: " + id + " has already been made.");
104                    }
105                }
106            }
107        }
108
109        /**
110         * Gets the number of times the {@link #onReceive} callback has been called for the
111         * {@link DownloadManager#ACTION_DOWNLOAD_COMPLETE} action, indicating the number of
112         * downloads completed thus far.
113         *
114         * @return the number of downloads completed so far.
115         */
116        public int numDownloadsCompleted() {
117            return mNumDownloadsCompleted;
118        }
119
120        /**
121         * Gets the list of download IDs.
122         * @return A Set<Long> with the ids of the completed downloads.
123         */
124        public Set<Long> getDownloadIds() {
125            synchronized(this) {
126                Set<Long> returnIds = new HashSet<Long>(downloadIds);
127                return returnIds;
128            }
129        }
130
131    }
132
133    public static class WiFiChangedReceiver extends BroadcastReceiver {
134        private Context mContext = null;
135
136        /**
137         * Constructor
138         *
139         * Sets the current state of WiFi.
140         *
141         * @param context The current app {@link Context}.
142         */
143        public WiFiChangedReceiver(Context context) {
144            mContext = context;
145        }
146
147        /**
148         * {@inheritDoc}
149         */
150        @Override
151        public void onReceive(Context context, Intent intent) {
152            if (intent.getAction().equalsIgnoreCase(ConnectivityManager.CONNECTIVITY_ACTION)) {
153                Log.i(LOG_TAG, "ConnectivityManager state change: " + intent.getAction());
154                synchronized (this) {
155                    this.notify();
156                }
157            }
158        }
159
160        /**
161         * Gets the current state of WiFi.
162         *
163         * @return Returns true if WiFi is on, false otherwise.
164         */
165        public boolean getWiFiIsOn() {
166            ConnectivityManager connManager = (ConnectivityManager)mContext.getSystemService(
167                    Context.CONNECTIVITY_SERVICE);
168            NetworkInfo info = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
169            Log.i(LOG_TAG, "WiFi Connection state is currently: " + info.isConnected());
170            return info.isConnected();
171        }
172    }
173
174    /**
175     * {@inheritDoc}
176     */
177    @Override
178    public void setUp() throws Exception {
179        mContext = getInstrumentation().getContext();
180        mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE);
181        mReceiver = registerNewMultipleDownloadsReceiver();
182    }
183
184    /**
185     * Helper to verify the size of a file.
186     *
187     * @param pfd The input file to compare the size of
188     * @param size The expected size of the file
189     */
190    protected void verifyFileSize(ParcelFileDescriptor pfd, long size) {
191        assertEquals(pfd.getStatSize(), size);
192    }
193
194    /**
195     * Helper to create and register a new MultipleDownloadCompletedReciever
196     *
197     * This is used to track many simultaneous downloads by keeping count of all the downloads
198     * that have completed.
199     *
200     * @return A new receiver that records and can be queried on how many downloads have completed.
201     */
202    protected MultipleDownloadsCompletedReceiver registerNewMultipleDownloadsReceiver() {
203        MultipleDownloadsCompletedReceiver receiver = new MultipleDownloadsCompletedReceiver();
204        mContext.registerReceiver(receiver, new IntentFilter(
205                DownloadManager.ACTION_DOWNLOAD_COMPLETE));
206        return receiver;
207    }
208
209    /**
210     * Enables or disables WiFi.
211     *
212     * Note: Needs the following permissions:
213     *  android.permission.ACCESS_WIFI_STATE
214     *  android.permission.CHANGE_WIFI_STATE
215     * @param enable true if it should be enabled, false if it should be disabled
216     */
217    protected void setWiFiStateOn(boolean enable) throws Exception {
218        Log.i(LOG_TAG, "Setting WiFi State to: " + enable);
219        WifiManager manager = (WifiManager)mContext.getSystemService(Context.WIFI_SERVICE);
220
221        manager.setWifiEnabled(enable);
222
223        String timeoutMessage = "Timed out waiting for Wifi to be "
224            + (enable ? "enabled!" : "disabled!");
225
226        WiFiChangedReceiver receiver = new WiFiChangedReceiver(mContext);
227        mContext.registerReceiver(receiver, new IntentFilter(
228                ConnectivityManager.CONNECTIVITY_ACTION));
229
230        synchronized (receiver) {
231            long timeoutTime = SystemClock.elapsedRealtime() + DEFAULT_MAX_WAIT_TIME;
232            boolean timedOut = false;
233
234            while (receiver.getWiFiIsOn() != enable && !timedOut) {
235                try {
236                    receiver.wait(DEFAULT_WAIT_POLL_TIME);
237
238                    if (SystemClock.elapsedRealtime() > timeoutTime) {
239                        timedOut = true;
240                    }
241                }
242                catch (InterruptedException e) {
243                    // ignore InterruptedExceptions
244                }
245            }
246            if (timedOut) {
247                fail(timeoutMessage);
248            }
249        }
250        assertEquals(enable, receiver.getWiFiIsOn());
251    }
252
253    /**
254     * Helper to enables or disables airplane mode. If successful, it also broadcasts an intent
255     * indicating that the mode has changed.
256     *
257     * Note: Needs the following permission:
258     *  android.permission.WRITE_SETTINGS
259     * @param enable true if airplane mode should be ON, false if it should be OFF
260     */
261    protected void setAirplaneModeOn(boolean enable) throws Exception {
262        int state = enable ? 1 : 0;
263
264        // Change the system setting
265        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON,
266                state);
267
268        String timeoutMessage = "Timed out waiting for airplane mode to be " +
269                (enable ? "enabled!" : "disabled!");
270
271        // wait for airplane mode to change state
272        int currentWaitTime = 0;
273        while (Settings.System.getInt(mContext.getContentResolver(),
274                Settings.Global.AIRPLANE_MODE_ON, -1) != state) {
275            timeoutWait(currentWaitTime, DEFAULT_WAIT_POLL_TIME, DEFAULT_MAX_WAIT_TIME,
276                    timeoutMessage);
277        }
278
279        // Post the intent
280        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
281        intent.putExtra("state", true);
282        mContext.sendBroadcast(intent);
283    }
284
285    /**
286     * Helper to wait for a particular download to finish, or else a timeout to occur
287     *
288     * Does not wait for a receiver notification of the download.
289     *
290     * @param id The download id to query on (wait for)
291     */
292    protected void waitForDownloadOrTimeout_skipNotification(long id) throws TimeoutException,
293            InterruptedException {
294        doWaitForDownloadsOrTimeout(new Query().setFilterById(id),
295                WAIT_FOR_DOWNLOAD_POLL_TIME, MAX_WAIT_FOR_DOWNLOAD_TIME);
296    }
297
298    /**
299     * Helper to wait for a particular download to finish, or else a timeout to occur
300     *
301     * Also guarantees a notification has been posted for the download.
302     *
303     * @param id The download id to query on (wait for)
304     */
305    protected void waitForDownloadOrTimeout(long id) throws TimeoutException,
306            InterruptedException {
307        waitForDownloadOrTimeout(id, WAIT_FOR_DOWNLOAD_POLL_TIME, MAX_WAIT_FOR_DOWNLOAD_TIME);
308    }
309
310    /**
311     * Helper to wait for a particular download to finish, or else a timeout to occur
312     *
313     * Also guarantees a notification has been posted for the download.
314     *
315     * @param id The download id to query on (wait for)
316     * @param poll The amount of time to wait
317     * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete
318     */
319    protected void waitForDownloadOrTimeout(long id, long poll, long timeoutMillis)
320            throws TimeoutException, InterruptedException {
321        doWaitForDownloadsOrTimeout(new Query().setFilterById(id), poll, timeoutMillis);
322        waitForReceiverNotifications(1);
323    }
324
325    /**
326     * Helper to wait for all downloads to finish, or else a specified timeout to occur
327     *
328     * Makes no guaranee that notifications have been posted for all downloads.
329     *
330     * @param poll The amount of time to wait
331     * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete
332     */
333    protected void waitForDownloadsOrTimeout(long poll, long timeoutMillis) throws TimeoutException,
334            InterruptedException {
335        doWaitForDownloadsOrTimeout(new Query(), poll, timeoutMillis);
336    }
337
338    /**
339     * Helper to wait for all downloads to finish, or else a timeout to occur, but does not throw
340     *
341     * Also guarantees a notification has been posted for the download.
342     *
343     * @param id The id of the download to query against
344     * @param poll The amount of time to wait
345     * @param timeoutMillis The max time (in ms) to wait for the download(s) to complete
346     * @return true if download completed successfully (didn't timeout), false otherwise
347     */
348    private boolean waitForDownloadOrTimeoutNoThrow(long id, long poll, long timeoutMillis) {
349        try {
350            doWaitForDownloadsOrTimeout(new Query().setFilterById(id), poll, timeoutMillis);
351            waitForReceiverNotifications(1);
352        } catch (TimeoutException e) {
353            return false;
354        }
355        return true;
356    }
357
358    /**
359     * Helper function to synchronously wait, or timeout if the maximum threshold has been exceeded.
360     *
361     * @param currentTotalWaitTime The total time waited so far
362     * @param poll The amount of time to wait
363     * @param maxTimeoutMillis The total wait time threshold; if we've waited more than this long,
364     *          we timeout and fail
365     * @param timedOutMessage The message to display in the failure message if we timeout
366     * @return The new total amount of time we've waited so far
367     * @throws TimeoutException if timed out waiting for SD card to mount
368     */
369    private int timeoutWait(int currentTotalWaitTime, long poll, long maxTimeoutMillis,
370            String timedOutMessage) throws TimeoutException {
371        long now = SystemClock.elapsedRealtime();
372        long end = now + poll;
373
374        // if we get InterruptedException's, ignore them and just keep sleeping
375        while (now < end) {
376            try {
377                Thread.sleep(end - now);
378            } catch (InterruptedException e) {
379                // ignore interrupted exceptions
380            }
381            now = SystemClock.elapsedRealtime();
382        }
383
384        currentTotalWaitTime += poll;
385        if (currentTotalWaitTime > maxTimeoutMillis) {
386            throw new TimeoutException(timedOutMessage);
387        }
388        return currentTotalWaitTime;
389    }
390
391    /**
392     * Helper to wait for all downloads to finish, or else a timeout to occur
393     *
394     * @param query The query to pass to the download manager
395     * @param poll The poll time to wait between checks
396     * @param timeoutMillis The max amount of time (in ms) to wait for the download(s) to complete
397     */
398    private void doWaitForDownloadsOrTimeout(Query query, long poll, long timeoutMillis)
399            throws TimeoutException {
400        int currentWaitTime = 0;
401        while (true) {
402            query.setFilterByStatus(DownloadManager.STATUS_PENDING | DownloadManager.STATUS_PAUSED
403                    | DownloadManager.STATUS_RUNNING);
404            Cursor cursor = mDownloadManager.query(query);
405
406            try {
407                if (cursor.getCount() == 0) {
408                    Log.i(LOG_TAG, "All downloads should be done...");
409                    break;
410                }
411                currentWaitTime = timeoutWait(currentWaitTime, poll, timeoutMillis,
412                        "Timed out waiting for all downloads to finish");
413            } finally {
414                cursor.close();
415            }
416        }
417    }
418
419    /**
420     * Synchronously waits for external store to be mounted (eg: SD Card).
421     *
422     * @throws InterruptedException if interrupted
423     * @throws Exception if timed out waiting for SD card to mount
424     */
425    protected void waitForExternalStoreMount() throws Exception {
426        String extStorageState = Environment.getExternalStorageState();
427        int currentWaitTime = 0;
428        while (!extStorageState.equals(Environment.MEDIA_MOUNTED)) {
429            Log.i(LOG_TAG, "Waiting for SD card...");
430            currentWaitTime = timeoutWait(currentWaitTime, DEFAULT_WAIT_POLL_TIME,
431                    DEFAULT_MAX_WAIT_TIME, "Timed out waiting for SD Card to be ready!");
432            extStorageState = Environment.getExternalStorageState();
433        }
434    }
435
436    /**
437     * Synchronously waits for a download to start.
438     *
439     * @param dlRequest the download request id used by Download Manager to track the download.
440     * @throws Exception if timed out while waiting for SD card to mount
441     */
442    protected void waitForDownloadToStart(long dlRequest) throws Exception {
443        Cursor cursor = getCursor(dlRequest);
444        try {
445            int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
446            int value = cursor.getInt(columnIndex);
447            int currentWaitTime = 0;
448
449            while (value != DownloadManager.STATUS_RUNNING &&
450                    (value != DownloadManager.STATUS_FAILED) &&
451                    (value != DownloadManager.STATUS_SUCCESSFUL)) {
452                Log.i(LOG_TAG, "Waiting for download to start...");
453                currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
454                        MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for download to start!");
455                cursor.requery();
456                assertTrue(cursor.moveToFirst());
457                columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
458                value = cursor.getInt(columnIndex);
459            }
460            assertFalse("Download failed immediately after start",
461                    value == DownloadManager.STATUS_FAILED);
462        } finally {
463            cursor.close();
464        }
465    }
466
467    /**
468     * Synchronously waits for our receiver to receive notification for a given number of
469     * downloads.
470     *
471     * @param targetNumber The number of notifications for unique downloads to wait for; pass in
472     *         -1 to not wait for notification.
473     * @throws Exception if timed out while waiting
474     */
475    private void waitForReceiverNotifications(int targetNumber) throws TimeoutException {
476        int count = mReceiver.numDownloadsCompleted();
477        int currentWaitTime = 0;
478
479        while (count < targetNumber) {
480            Log.i(LOG_TAG, "Waiting for notification of downloads...");
481            currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
482                    MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for download notifications!"
483                    + " Received " + count + "notifications.");
484            count = mReceiver.numDownloadsCompleted();
485        }
486    }
487
488    /**
489     * Synchronously waits for a file to increase in size (such as to monitor that a download is
490     * progressing).
491     *
492     * @param file The file whose size to track.
493     * @throws Exception if timed out while waiting for the file to grow in size.
494     */
495    protected void waitForFileToGrow(File file) throws Exception {
496        int currentWaitTime = 0;
497
498        // File may not even exist yet, so wait until it does (or we timeout)
499        while (!file.exists()) {
500            Log.i(LOG_TAG, "Waiting for file to exist...");
501            currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
502                    MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for file to be created.");
503        }
504
505        // Get original file size...
506        long originalSize = file.length();
507
508        while (file.length() <= originalSize) {
509            Log.i(LOG_TAG, "Waiting for file to be written to...");
510            currentWaitTime = timeoutWait(currentWaitTime, WAIT_FOR_DOWNLOAD_POLL_TIME,
511                    MAX_WAIT_FOR_DOWNLOAD_TIME, "Timed out waiting for file to be written to.");
512        }
513    }
514
515    /**
516     * Helper to remove all downloads that are registered with the DL Manager.
517     *
518     * Note: This gives us a clean slate b/c it includes downloads that are pending, running,
519     * paused, or have completed.
520     */
521    protected void removeAllCurrentDownloads() {
522        Log.i(LOG_TAG, "Removing all current registered downloads...");
523        Cursor cursor = mDownloadManager.query(new Query());
524        try {
525            if (cursor.moveToFirst()) {
526                do {
527                    int index = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
528                    long downloadId = cursor.getLong(index);
529
530                    mDownloadManager.remove(downloadId);
531                } while (cursor.moveToNext());
532            }
533        } finally {
534            cursor.close();
535        }
536    }
537
538    /**
539     * Helper to verify an int value in a Cursor
540     *
541     * @param cursor The cursor containing the query results
542     * @param columnName The name of the column to query
543     * @param expected The expected int value
544     */
545    private void verifyInt(Cursor cursor, String columnName, int expected) {
546        int index = cursor.getColumnIndex(columnName);
547        int actual = cursor.getInt(index);
548        assertEquals(expected, actual);
549    }
550
551    /**
552     * Performs a query based on ID and returns a Cursor for the query.
553     *
554     * @param id The id of the download in DL Manager; pass -1 to query all downloads
555     * @return A cursor for the query results
556     */
557    protected Cursor getCursor(long id) throws Exception {
558        Query query = new Query();
559        if (id != -1) {
560            query.setFilterById(id);
561        }
562
563        Cursor cursor = mDownloadManager.query(query);
564        int currentWaitTime = 0;
565
566        try {
567            while (!cursor.moveToFirst()) {
568                Thread.sleep(DEFAULT_WAIT_POLL_TIME);
569                currentWaitTime += DEFAULT_WAIT_POLL_TIME;
570                if (currentWaitTime > DEFAULT_MAX_WAIT_TIME) {
571                    fail("timed out waiting for a non-null query result");
572                }
573                cursor.requery();
574            }
575        } catch (Exception e) {
576            cursor.close();
577            throw e;
578        }
579        return cursor;
580    }
581}
582