Detailed analysis of resumable download for Android 2
Since a blog cannot be written, this is followed by the previous blog.
After finishing the View in MVC, we need to consider the Control layer. His task is to use multithreading in the background to implement breakpoint download.
First look at the source code:
Public class FileDownloader {/* TAG for debugging */private static final String TAG = "FileDownloader";/* Context */private context Context; /* operation on the database */private FileService fileService;/* Stop download */private boolean exit;/* length of the downloaded file */private int downloadSize = 0; /* length of the original File */private int fileSize = 0;/* Number of threads */private DownloadThread [] threads;/* save the File locally */private File saveFile; /* cache the download length of each thread */private Map
Data = new ConcurrentHashMap
();/* Download length of each thread */private int block;/* download path */private String downloadUrl; /*** get thread count */public int getThreadSize () {return threads. length;}/*** exit download */public void exit () {this. exit = true;} public boolean getExit () {return this. exit;}/*** get file size * @ return file size */public int getFileSize () {return fileSize ;} /*** accumulative downloaded size * @ param size */protected synchronized void append (int size) {downloadSize + = size ;}/* ** Update the last download location of the specified Thread * @ param threadId thread id * @ param pos the last download location */protected synchronized void update (int threadId, int pos) {this. data. put (threadId, pos); this. fileService. update (this. downloadUrl, threadId, pos );} /*** build a file downloader * @ param downloadUrl download path * @ param fileSaveDir file storage directory * @ param threadNum Number of download threads */public FileDownloader (Context context, String downloadUrl, file fileSaveDir, int threadNum) {try {This. context = context; this. downloadUrl = downloadUrl; fileService = new FileService (this. context); URL url = new URL (this. downloadUrl); if (! FileSaveDir. exists () fileSaveDir. mkdirs (); // create this if the directory does not exist. threads = new DownloadThread [threadNum]; HttpURLConnection conn = (HttpURLConnection) url. openConnection (); conn. setConnectTimeout (5*1000); conn. setRequestMethod ("GET"); conn. setDoInput (true); conn. setRequestProperty ("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml + xml, application/vnd. m S-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd. ms-excel, application/vnd. ms-powerpoint, application/msword, */* "); conn. setRequestProperty ("Accept-Language", "zh-CN"); conn. setRequestProperty ("Referer", downloadUrl); conn. setRequestProperty ("Charset", "UTF-8"); conn. setRequestProperty ("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0 ;. NET CLR 1.1.4322 ;. net clr 2.0.50727 ;. net clr 3.0.04506.30 ;. net clr 3.0.20.6.2152 ;. net clr 3.5.30729) "); conn. setRequestProperty ("Connection", "Keep-Alive");/** the function of the connect () method first creates an object and then establishes a Connection. After creating an object, you can specify various options (such as doInput and * UseCaches) before establishing a connection ). An error occurs when you set the connection. The operation that can be performed only after the connection (for example, * getContentLength). If necessary, the connection is implicitly executed. */Conn. connect (); if (conn. getResponseCode () = 200) {this. fileSize = conn. getContentLength (); // obtain the total file size based on the response if (this. fileSize <= 0) throw new RuntimeException ("Unkown file size"); String filename = getFileName (conn); // get the file name this. saveFile = new File (fileSaveDir, filename); // construct a Map to save the File
Logdata = fileService. getData (downloadUrl); // obtain the download record if (logdata. size ()> 0) {// If a download record exists, it is important to put the length of data downloaded by each thread into data, for example, if the user closes and exits the application for a certain reason, // when the user enters the application again, the download should start from the last unfinished place instead of the new download for (Map. entry
Entry: logdata. entrySet () data. put (entry. getKey (), entry. getValue ();} if (this. data. size () = this. threads. length) {// calculate the total length of data downloaded by all threads for (int I = 0; I <this. threads. length; I ++) {this. downloadSize + = this. data. get (I + 1) ;}/// calculate the length of data downloaded by each thread. this. block = (this. fileSize % this. threads. length) = 0? This. fileSize/this. threads. length: this. fileSize/this. threads. length + 1;} else {throw new RuntimeException ("server no response") ;}} catch (Exception e) {throw new RuntimeException ("don't connection this url");}/*** get file name */private String getFileName (HttpURLConnection conn) {/** use the Suffix in the URL address as the file name. For example, the URL is http: // 192.162.1.1: 8080/web/lenver.exe, and * another name is called lenver.exe */String filename = this. download Url. substring (this. downloadUrl. lastIndexOf ('/') + 1); if (filename = null | "". equals (filename. trim () {// if the file name for (int I = 0; I ++) {String mine = conn. getHeaderField (I); if (mine = null) break; /** when the requested path's obtained name is invalid, the shih * Content-disposition can actually control the user's requested Content to be saved as a file and provide a default file name, * The file is displayed directly in the browser or the File Download Dialog Box is displayed during access. */If ("content-disposition ". equals (conn. getHeaderFieldKey (I ). toLowerCase () {Matcher m = Pattern. compile (". * filename = (. *)"). matcher (mine. toLowerCase (); if (m. find () return m. group (1) ;}} filename = UUID. randomUUID () + ". tmp "; // get a file name by default} return filename;}/*** start to download the file ** @ param listener * listen for changes in the number of downloads, if you do not need to know the number of real-time downloads, you can set it to null * @ return downloaded file size * @ throws Exception */public int download (DownloadProgressLi Stener listener) throws Exception {try {RandomAccessFile randOut = new RandomAccessFile (this. saveFile, "rw"); if (this. fileSize> 0) randOut. setLength (this. fileSize); randOut. close (); URL url = new URL (this. downloadUrl); if (this. data. size ()! = This. threads. length) {// if the original number of threads has not been downloaded or the number of original download threads is different from the current number of threads, this is all three. data. clear (); for (int I = 0; I <this. threads. length; I ++) {this. data. put (I + 1, 0); // initialize the length of data downloaded by each thread to 0} this. downloadSize = 0 ;}for (int I = 0; I <this. threads. length; I ++) {// enable the thread to download int downLength = this. data. get (I + 1); // extract the download length of a thread from the database if (downLength <this. block & this. downloadSize <this. fileSize) {// determine whether the thread has completed the download; otherwise, continue to download this. Threads [I] = new DownloadThread (this, url, this. saveFile, this. block, this. data. get (I + 1), I + 1); this. threads [I]. setPriority (7); // sets the priority this. threads [I]. start (); // start thread} else {this. threads [I] = null;} fileService. delete (this. downloadUrl); // if there are download records, delete them and add fileService again. save (this. downloadUrl, this. data); boolean notFinish = true; // download unfinished while (notFinish) // This loop is critical, and he can maintain the thread of his caller, namely DownloadTask, to keep running, Then you can continuously send messages to the UI Thread {// cyclically determine whether all threads have completed downloading the Thread. sleep (900); notFinish = false; // assume that all threads have been downloaded for (int I = 0; I <this. threads. length; I ++) {if (this. threads [I]! = Null &&! This. threads [I]. isFinish () {// if the thread is not completed, download notFinish = true; // set the flag to download if (this. threads [I]. getDownLength () =-1) {// If the download fails, download this again. threads [I] = new DownloadThread (this, url, this. saveFile, this. block, this. data. get (I + 1), I + 1); this. threads [I]. setPriority (7); this. threads [I]. start () ;}} if (listener! = Null) listener. onDownloadSize (this. downloadSize); // notify the data length that has been downloaded} if (downloadSize = this. fileSize) fileService. delete (this. downloadUrl); // The deletion record after the download is completed} catch (Exception e) {throw new Exception ("file download error");} return this. downloadSize;}/*** get the Http response header field ** @ param http * @ return */public static Map
GetHttpResponseHeader (HttpURLConnection http) {Map
Header = new LinkedHashMap
(); For (int I = 0; I ++) {String mine = http. getHeaderField (I); if (mine = null) break; header. put (http. getHeaderFieldKey (I), mine) ;}return header ;}}
The FileService class is a class used to operate databases. As described in the next blog, it is used to add, delete, and delete databases.
Key code analysis:
Private Map Data = new ConcurrentHashMap (); This parameter is used to read the value when every thread stops downloading from the database. After the download is complete, the record is deleted.
We store the location of the downloaded file under the root directory of the SD card. You can see that we have uploaded the location of the SD card as a parameter to FIleDownloader, then, get the final '\' string in the URL as the file name. If the file name does not exist, use the default name provided by the server. You can refer to the getName () method.
Then, the download method in FileDownlaoder is called in MainActivity. It can be seen that three threads are enabled to complete the download function. Then you can see that the downloaded length of the corresponding thread will be read from the database before each download, and then compare it with the length of the download he should have been downloaded, if the thread completes the task, that is, the read length from the database is equal to the length of the download, it does not need to be downloaded; otherwise, it will continue. Then there will be a loop, which is an endless loop by default. It does not take 900 milliseconds to check whether the download is complete, whether the download fails, and send the latest download progress to the UI thread. You can set the sleep time to any value, but it should be appropriate, because users generally need to check the download progress after a certain period of time, we try to update the UI as soon as possible to give users a better experience, however, it cannot be too frequent because the execution times are too many, compromising performance. You can measure the size of the downloaded file. If you are older, you can update the Ui a little longer, but if you are smaller, you can update the UI a little faster.
Then, FIleDownloader is actually a class that controls the threads that actually download files. The real download class is DownloadThread. The next blog will share his usage with you.