Easy Tutorial
❮ Verilog2 Pli Intro Android Tutorial Callback Event Handle ❯

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?

>

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&lt;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&lt;Integer, Integer> data = new ConcurrentHashMap&lt;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.4 ImageView (Image View)

-2.3.5 RadioButton (Radio Button) & Checkbox (Checkbox)

-2.3.6 ToggleButton (Toggle Button) and Switch (Switch)

-2.3.7 ProgressBar (Progress Bar)

-2.3.8 SeekBar (Seek Bar)

-2.3.9 RatingBar (Rating Bar)

-2.4.1 ScrollView (Scroll View)

-[2.4.

Follow on WeChat

❮ Verilog2 Pli Intro Android Tutorial Callback Event Handle ❯