7.3.3 Android File Downloading (2)
Category Android Basic Tutorial
Introduction to This Section:
>
This section brings you an analysis of the code for multi-threaded breakpoint resumption in Android. Hehe, why is it called an analysis? Because I can't write it out either, (╯□╰)! Let's first talk about the meaning of the breakpoint. The so-called breakpoint is: using a database to record the progress of each thread's download every day! Each time it starts, it queries the download progress of a thread based on the thread ID and continues to download! It sounds quite simple, and it's normal if you can't write it out, so it's best to understand this section, if you can't understand it, it's also okay, just be able to use and modify it!
Okay, let's start the content of this section~
Analysis of the Code Process for Multi-threaded Breakpoint Download in Android:
Running Effect Picture :
Complete Analysis of the Implementation Process :
Step 1: Create a Table to Record Thread Download Information
Create a database table, so we create a database manager class, inherit the SQLiteOpenHelper class, and override the onCreate() and onUpgrade() methods. The fields of the table we created are as follows:
DBOpenHelper.java :
package com.jay.example.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
public class DBOpenHelper extends SQLiteOpenHelper {
public DBOpenHelper(Context context) {
super(context, "downs.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
//The database structure is: table name: filedownlog fields: id, downpath: the current download resource,
//threadid: the ID of the download thread, downlength: the last position of the thread download
db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog " +
"(id integer primary key autoincrement," +
" downpath varchar(100)," +
" threadid INTEGER, downlength INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//When the version number changes, this method is called. Here, the data table is deleted. In actual business, data backup is usually required.
db.execSQL("DROP TABLE IF EXISTS filedownlog");
onCreate(db);
}
}
Step 2: Create a Database Operation Class
What kind of methods do we need to create?
>
① We need a method to get the current download length of each thread according to the URL.
② Next, when our thread is newly opened, we need a method to insert parameters related to the thread into the database.
③ We also need to define a method that can update the download file length in real time.
④ After our thread finishes downloading, we also need a method to delete the corresponding record based on the thread ID.
FileService.java
package com.jay.example.db;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
/*
* This class is a business bean class, completing database-related operations
* */
public class FileService {
//Declare the database manager
private DBOpenHelper openHelper;
//In the constructor, instantiate the database manager based on the context object
public FileService(Context context) {
openHelper = new DBOpenHelper(context);
}
/**
* Get the downloaded file length of each thread for a specified URI
* @param path
* @return
* */
public Map<Integer, Integer> getData(String path)
{
//Get a readable database handle, which is usually a writable database handle returned by the internal implementation
SQLiteDatabase db = openHelper.getReadableDatabase();
//Query all the current download data based on the download path, and the Cursor points before the first record
Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?",
new String[]{path});
//Establish a hash table to store the downloaded file length of each thread
Map<Integer,Integer> data = new HashMap<Integer, Integer>();
//Start iterating the Cursor object from the first record
cursor.moveToFirst();
while(cursor.moveToNext())
{
//Store the thread ID and the length of the thread that has been downloaded in the data hash table
data.put(cursor.getInt(0), cursor.getInt(1));
data.put(cursor.getInt(cursor.getColumnIndexOrThrow("threadid")),
cursor
import com.jay.example.db.FileService;
public class FileDownloader {
private static final String TAG = "File Downloader"; //Set a tag for logging
private static final int RESPONSE_OK = 200; //Set response code to 200, representing a successful access
private FileService fileService; //Obtain the business bean of the local database
private boolean exited; //Flag to stop downloading
private Context context; //The context object of the program
private int downloadedSize = 0; //The length of the downloaded file
private int fileSize = 0; //The initial length of the file
private DownloadThread[] threads; //Download thread pool set according to the number of threads
private File saveFile; //Save data to a local file
private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>(); //Cache the download length of each thread
private int block; //The length of data each thread downloads
private String downloadUrl; //Download path
/**
* Get the number of threads
*/
public int getThreadSize()
{
//return threads.length;
return 0;
}
/**
* Exit download
* */
public void exit()
{
this.exited = true; //Set the exit flag to true;
}
public boolean getExited()
{
return this.exited;
}
/**
* Get the size of the file
* */
public int getFileSize()
{
return fileSize;
}
/**
* Accumulate the size of the downloaded data
* Use a synchronized lock to solve concurrent access issues
* */
protected synchronized void append(int size)
{
//Add the real-time download length to the total download length
downloadedSize += size;
}
/**
* Update the last download position of the specified thread
* @param threadId Thread ID
* @param pos The last downloaded position
* */
protected synchronized void update(int threadId, int pos)
{
//Assign the latest download length to the specified thread ID, and the previous value will be overwritten
this.data.put(threadId, pos);
//Update the download length of the specified thread in the database
this.fileService.update(this.downloadUrl, threadId, pos);
}
/**
* Build the file downloader
* @param downloadUrl Download path
* @param fileSaveDir The directory where the file is saved
* @param threadNum The number of download threads
* @return
*/
public FileDownloader(Context context, String downloadUrl, File fileSaveDir, int threadNum)
{
try {
this.context = context; //Get the context object and assign it
this.downloadUrl = downloadUrl; //Assign the download path
fileService = new FileService(this.context); //Instantiate the business bean class for database operations, passing a context value
URL url = new URL(this.downloadUrl); //Instantiate the URL based on the download path
if (!fileSaveDir.exists()) fileSaveDir.mkdir(); //If the file does not exist, specify the directory, which can create multiple directories here
this.threads = new DownloadThread[threadNum]; //Create a download thread pool based on the number of download threads
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //Create a remote connection handle, which is not actually connected here
conn.setConnectTimeout(5000); //Set the connection timeout to 5 seconds
conn.setRequestMethod("GET"); //Set the request method to GET
//Set the media types that the client can accept
conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, " +
"image/x-png, application/x-shockwave-flash, application/xaml+xml, " +
"application/vnd.ms-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"); //Set the user's language
conn.setRequestProperty("Referer", downloadUrl); //Set the source page of the request for server-side source statistics
conn.setRequestProperty("Charset", "UTF-8"); //Set client encoding
//Set user agent
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;" +
private String getFileName(HttpURLConnection conn) {
// Extract the filename from the download URL string
String filename = this.downloadUrl.substring(this.downloadUrl.lastIndexOf('/') + 1);
if (filename == null || "".equals(filename.trim())) { // If the filename cannot be obtained
for (int i = 0;; i++) { // Use an infinite loop to traverse
String mine = conn.getHeaderField(i); // Get the value of the header field at a specific index from the returned stream
if (mine == null) break; // If the end of the returned header is reached, exit the loop
// Get the content-disposition return field, which may contain the filename
if ("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())) {
// Use a regular expression to query the filename
Matcher m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());
if (m.find()) return m.group(1); // If there is a string that matches the regular expression rule, return it
}
}
filename = UUID.randomUUID() + ".tmp"; // If not found, take a default filename
// A 16-byte binary number generated by the unique number of the network card (each network card has a unique identifier) and the unique number of CPU time is used as the filename
}
return filename;
}
/**
* Start downloading the file
* @param listener Listens for changes in the download amount. If you don't need to know the real-time download amount, it can be set to null
* @return The size of the downloaded file
* @throws Exception
*/
// Start downloading, and if there is an exception, throw it to the caller
public int download(DownloadProgressListener listener) throws Exception {
try {
RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rwd");
// Set the file size
if (this.fileSize > 0) randOut.setLength(this.fileSize);
randOut.close(); // Close the file to take effect
URL url = new URL(this.downloadUrl);
if (this.data.size() != this.threads.length) {
// If there was no previous download or the number of download threads was inconsistent with the current number of threads
this.data.clear();
// Traverse the thread pool
for (int i = 0; i < this.threads.length; i++) {
this.data.put(i + 1, 0); // Initialize the length of data already downloaded by each thread to 0
}
this.downloadedSize = 0; // Set the length already downloaded to 0
}
for (int i = 0; i < this.threads.length; i++) { // Start the thread for downloading
int downLength = this.data.get(i + 1);
// Get the length of data already downloaded by the specific thread id
// If the thread has not completed the download, continue downloading
if (downLength < this.block && this.downloadedSize < this.fileSize) {
// Initialize the thread with a specific id
this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i + 1), i + 1);
// Set the thread priority, Thread.NORM_PRIORITY = 5;
// Thread.MIN_PRIORITY = 1; Thread.MAX_PRIORITY = 10, the larger the value, the higher the priority
this.threads[i].setPriority(7);
this.threads[i].start(); // Start the thread
} else {
this.threads[i] = null; // Indicates that the thread has completed the download task
}
}
fileService.delete(this.downloadUrl);
// If there is a download record, delete them and then add them again
fileService.save(this.downloadUrl, this.data);
// Write the real-time download data into the database
boolean notFinish = true;
// Download not finished
while (notFinish) {
// Loop to determine if all threads have completed the download
Thread.sleep(900);
notFinish = false;
// Assume all threads have finished downloading
for (int i = 0; i < this.threads.length; i++) {
if (this.threads[i] != null && !this.threads[i].isFinish()) {
// If a thread is found that has not finished downloading
notFinish = true;
// Set the flag that the download is not finished
if (this.threads[i].getDownLength() == -1) {
// If the download fails, download again based on the length of data already downloaded
// Open a download thread again, set the thread priority
this.threads[i] = new Download
English:
```java
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import android.util.Log;
public class DownloadThread extends Thread {
private static final String TAG = "Download Thread Class"; // Define TAG for marking when printing log
private File saveFile; // The file where the downloaded data is saved
private URL downUrl; // The URL for download
private int block; // The size of data each thread downloads
private int threadId = -1; // Initialize thread ID
private int downLength; // The length of data this thread has downloaded
private boolean finish = false; // Flag indicating whether this thread has finished downloading
private FileDownloadered downloader; // File downloader
public DownloadThread(FileDownloadered downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) {
this.downUrl = downUrl;
this.saveFile = saveFile;
this.block = block;
this.downloader = downloader;
this.threadId = threadId;
this.downLength = downLength;
}
@Override
public void run() {
if(downLength < block){ // Not finished downloading
try {
HttpURLConnection http = (HttpURLConnection) downUrl.openConnection();
http.setConnectTimeout(5 * 1000);
http.setRequestMethod("GET");
http.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/x-png, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
http.setRequestProperty("Accept-Language", "zh-CN");
http.setRequestProperty("Referer", downUrl.toString());
http.setRequestProperty("Charset", "UTF-8");
int startPos = block * (threadId - 1) + downLength; // Start position
int endPos = block * threadId - 1; // End position
http.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos); // Set the range for the entity data
http.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.4506.2152; .NET CLR 3.5.30729)");
http.setRequestProperty("Connection", "Keep-Alive");
InputStream inStream = http.getInputStream(); // Get the input stream from the remote connection
byte[] buffer = new byte[1024]; // Set the local data buffer size to 1MB
int offset = 0; // The amount of data read each time
print("Thread " + this.threadId + " starts downloading from position " + startPos); // Print the starting position of this thread's download
RandomAccessFile threadfile = new RandomAccessFile(this.saveFile, "rwd");
threadfile.seek(startPos);
// While the user has not requested to stop the download and has not reached the end of the requested data, it will keep reading data in a loop
while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) {
threadfile.write(buffer, 0, offset); // Directly write data into the file
downLength += offset; // Add the data written by the new thread to the file to the download length
downloader.update(this.threadId, downLength); // Update the length of data this thread has downloaded to the database and memory hash table
downloader.append(offset); // Add the length of the newly downloaded data to the total length of downloaded data
}
threadfile.close();
inStream.close();
print("Thread " + this.threadId + " download finished");
this.finish = true; // Set the completion flag to true, whether the download is finished or the user has actively interrupted the download
} catch (Exception e) {
this.downLength = -1; // Set the length of data this thread has downloaded to -1
print("Thread " + this.threadId + ":" + e);
}
}
}
private static void print(String msg){
Log.i(TAG, msg);
}
/**
* Check if the download is complete
* @return
*/
public boolean isFinish() {
return finish;
```xml
<LinearLayout
android:layout_height="18dp"
style="?android:attr/progressBarStyleHorizontal"
android:id="@+id/progressBar"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:id="@+id/textresult"
android:text="Display the percentage of real-time downloads"
/>
</LinearLayout>
Step 7: Writing MainActivity
Finally, we have our MainActivity, which completes the initialization of components and related variables; Use a handler to update the interface, and time-consuming operations cannot be performed on the main thread, So you need to open a new thread here, implemented with Runnable, see the code for details.
MainActivity.java:
package com.jay.example.multhreadcontinuabledemo;
import java.io.File;
import com.jay.example.service.FileDownloadered;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private EditText editpath;
private Button btndown;
private Button btnstop;
private TextView textresult;
private ProgressBar progressbar;
private static final int PROCESSING = 1; // Message flag for real-time data transfer during download
private static final int FAILURE = -1; // Message flag when download fails
private Handler handler = new UIHander();
private final class UIHander extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
// During download
case PROCESSING:
int size = msg.getData().getInt("size"); // Get the length of the downloaded data from the message
progressbar.setProgress(size); // Set the progress of the progress bar
// Calculate the downloaded percentage, here you need to convert it to a floating point number for calculation
float num = (float) progressbar.getProgress() / (float) progressbar.getMax();
int result = (int) (num * 100); // Convert the obtained floating point calculation result to an integer
textresult.setText(result + "%"); // Display the download percentage on the interface control
if (progressbar.getProgress() == progressbar.getMax()) { // Prompt when download is completed
Toast.makeText(getApplicationContext(), "File download successful", 1).show();
}
break;
case FAILURE: // Prompt when download fails
Toast.makeText(getApplicationContext(), "File download failed", 1).show();
break;
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editpath = (EditText) findViewById(R.id.editpath);
btndown = (Button) findViewById(R.id.btndown);
btnstop = (Button) findViewById(R.id.btnstop);
textresult = (TextView) findViewById(R.id.textresult);
progressbar = (ProgressBar) findViewById(R.id.progressBar);
ButtonClickListener listener = new ButtonClickListener();
btndown.setOnClickListener(listener);
btnstop.setOnClickListener(listener);
}
private final class ButtonClickListener implements View.OnClickListener {
public void onClick(View v) {
switch (v.getId()) {
case R.id.btndown:
String path = editpath.getText().toString();
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File saveDir = Environment.getExternalStorageDirectory();
download(path, saveDir);
} else {
Toast.makeText(getApplicationContext(), "Failed to read SD card", 1).show();
}
btndown.setEnabled(false);
btnstop.setEnabled(true);
break;
case R.id.btnstop:
exit();
btndown.setEnabled(true);
btnstop.setEnabled(false);
break;
}
}
/*
Since user input events (clicking a button, touching the screen...) are handled by the main thread, if the main thread is busy,
and the user-generated input events are not processed within 5 seconds, the system will report an "Application Not Responding" error.
Therefore, you should not perform a time-consuming task in the main thread, otherwise it will block the main thread and be unable to handle user input events,
leading to the occurrence of the "Application Not Responding" error. Time-consuming tasks should be performed in a sub-thread.
*/
private DownloadTask task;
/**
* Exit download
*/
public void exit() {
if (task != null) task.exit();
}
private void download(String path, File saveDir) { // Runs in the main thread
task = new DownloadTask(path, saveDir);
new Thread(task).start();
}
}
---
### Step 8: Add relevant permissions in the AndroidManifest.xml file
<!-- Internet access permission --> <uses-permission android:name="android.permission.INTERNET"/> <!-- Permission to create and delete files on the SDCard --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <!-- Permission to write data to the SDCard --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ```
Download Reference Code:
Multithreaded breakpoint downloader demo: MulThreadContinuableDemo.zip
Multithreaded breakpoint download + online music player: Multithreaded Breakpoint Download + Online Music Player.zip
Summary of This Section:
>
Well, that's all for the code analysis of Android multithreading breakpoint downloading in this section. It's quite a lot to take in, isn't it? However, it's still the same saying, why reinvent the wheel when others have already created it? Besides, we currently may not have the capability to create one, right? So, for now, it's good enough to understand, know how to use, and be aware of how to modify it. That's all I have to say, thank you~
-1.0 Android Basic Tutorial Introduction
-1.0.1 Latest Android Basic Tutorial Table of Contents for 2015
-1.1 Background and System Architecture Analysis
-1.2 Development Environment Setup
-1.2.1 Developing Android APP with Eclipse + ADT + SDK
-1.2.2 Developing Android APP with Android Studio
-1.3 Solving SDK Update Issues
-1.4 Genymotion Emulator Installation
-1.5.1 Git Tutorial on Basic Operations of Local Repositories
-1.5.2 Git: Setting Up a Remote Repository with GitHub
-1.6 How to Play with 9 (Jiu Mei) Images
-1.7 Interface Prototype Design
-1.8 Project Related Analysis (Various Files, Resource Access)
-1.9 Android App Signing and Packaging
-1.11 Decompiling APK to Retrieve Code & Resources
-2.1 The Concept of View and ViewGroup
-2.2.1 LinearLayout (Linear Layout)
-2.2.2 RelativeLayout (Relative Layout)
-2.2.3 TableLayout (Table Layout)
-2.2.4 FrameLayout (Frame Layout)
-2.2.5 GridLayout (Grid Layout)
-2.2.6 AbsoluteLayout (Absolute Layout)
-2.3.1 TextView (Text Box) Detailed Explanation
-2.3.2 EditText (Input Box) Detailed Explanation
-2.3.3 Button (Button) and ImageButton (Image Button)
-2.3.5 RadioButton (Radio Button) & Checkbox (Checkbox)
-2.3.6 ToggleButton (Toggle Button) and Switch (Switch)
-2.3.7 ProgressBar (Progress Bar)
-2.4.1 ScrollView (Scroll View)
-[2.4.
4.4.2 Further Exploration of ContentProvider - Document Provider
5.2.1 Detailed Explanation of Fragment Example - Implementation of Bottom Navigation Bar (Method 1)
5.2.2 Detailed Explanation of Fragment Example - Implementation of Bottom Navigation Bar (Method 2)
5.2.3 Detailed Explanation of Fragment Example - Implementation of Bottom Navigation Bar (Method 3)
6.2 Data Storage and Access - SharedPreferences to Save User Preferences
7.1.1 What to Learn in Android Network Programming and Http Protocol Study
7.3.3 Android File Download (2)
11.0 "2015 Latest Android Basic Tutorial" Concludes with a Celebration~
12.2 DrySister Girl Viewing App (First Edition) - 2. Parsing Backend Data
12.4 DrySister Girl Viewing App (First Edition) - 4. Adding Data Caching (Incorporating SQLite)