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