Download files with Retrofit2 & Progress indicator

I wanted to download files in Android and have cool stuff like notification with progress while the file is downloading and easy way for it to be scanned by the media library. The first way was to use the DownloadManager class

DownloadManager

Well, the problems with the DownloadManager is that it doesn’t always download the files you tell it to. I faced several issues with it on devices with older Android versions. It would start the download and then fail. It is really hard to debug why it fails, but the same DownloadManager works on Android 5.0+. That’s why I moved to the other solution.

IntentService, Retrofit & Progress indicator

I found THIS great tutorial online. It is about how to build a download service using IntentService with RetroFit and a progress notification. I am copy-pasting the tutorial below.

Adding Dependencies

Here we use Jake Wharton’s Butter Knife library for view binding. For Butter Knife’s annotations to work properly we need to add Android apt plugin. Open the project’s top level Gradle build file and add the following dependency.

classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’

Then open the app module’s Gradle build file and apply the plugin,

apply plugin: ‘com.neenbedankt.android-apt’

The dependencies we require for Retrofit, Butter Knife and Design Support library are ,

compile ‘com.android.support:design:23.4.0’

compile ‘com.squareup.retrofit2:retrofit:2.0.2’

compile ‘com.jakewharton:butterknife:8.0.1’

apt ‘com.jakewharton:butterknife-compiler:8.0.1’build.gradle

build.gradle

Top level gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
 
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
 
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
 
allprojects {
    repositories {
        jcenter()
    }
}
 
task clean(type: Delete) {
    delete rootProject.buildDir
}

Inner gradle (apps)

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
 
    defaultConfig {
        applicationId "com.learn2crack.filedownload"
        minSdkVersion 21
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
 
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support:design:23.4.0'
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
    compile 'com.jakewharton:butterknife:8.0.1'
    apt 'com.jakewharton:butterknife-compiler:8.0.1'
}

Creating Layout

Our main layout has a Button to start download, ProgressBar to display Progress, TextView to display data downloaded.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    android:id="@+id/coordinatorLayout"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
 
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.learn2crack.filedownload.MainActivity">
 
        <android.support.v7.widget.AppCompatButton
            android:id="@+id/btn_download"
            android:layout_centerVertical="true"
            android:text="Start Download"
            android:background="@color/colorPrimary"
            android:textColor="@android:color/white"
            android:drawableLeft="@drawable/ic_download"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
 
        <ProgressBar
            android:id="@+id/progress"
            style="@style/Widget.AppCompat.ProgressBar.Horizontal"
            android:layout_marginTop="48dp"
            android:layout_below="@+id/btn_download"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
 
        <TextView
            android:id="@+id/progress_text"
            android:layout_below="@+id/progress"
            android:textAlignment="center"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
 
    </RelativeLayout>
 
</android.support.design.widget.CoordinatorLayout>

Creating Model class

Our Download Model class has three int fields for progress percentage, file size while downloading and total file size. Generate Getter and Setter for the fields.

We send this Download model class object in a Intent for broadcasting. Inorder to send this in Intent Extra we need to implement the Parcelable interface. The describeContents() and writeToParcel() methods should be overrided. It also has a static Parcelable CREATOR field which implements Parcelable.Creator interface.

Download.java

package com.learn2crack.filedownload.models;
 
import android.os.Parcel;
import android.os.Parcelable;
 
public class Download  implements Parcelable{
 
    public Download(){
 
    }
 
    private int progress;
    private int currentFileSize;
    private int totalFileSize;
 
    public int getProgress() {
        return progress;
    }
 
    public void setProgress(int progress) {
        this.progress = progress;
    }
 
    public int getCurrentFileSize() {
        return currentFileSize;
    }
 
    public void setCurrentFileSize(int currentFileSize) {
        this.currentFileSize = currentFileSize;
    }
 
    public int getTotalFileSize() {
        return totalFileSize;
    }
 
    public void setTotalFileSize(int totalFileSize) {
        this.totalFileSize = totalFileSize;
    }
 
    @Override
    public int describeContents() {
        return 0;
    }
 
    @Override
    public void writeToParcel(Parcel dest, int flags) {
 
        dest.writeInt(progress);
        dest.writeInt(currentFileSize);
        dest.writeInt(totalFileSize);
    }
 
    private Download(Parcel in) {
 
        progress = in.readInt();
        currentFileSize = in.readInt();
        totalFileSize = in.readInt();
    }
 
    public static final Parcelable.Creator<Download> CREATOR = new Parcelable.Creator<Download>() {
        public Download createFromParcel(Parcel in) {
            return new Download(in);
        }
 
        public Download[] newArray(int size) {
            return new Download[size];
        }
    };
}

Creating Retrofit Interface

 

Here I am going to download the file

https://download.learn2crack.com/files/Node-Android-Chat.zip

For downloading we use GET method. For downloading large files we need to add @Streaming annotation to Retrofit Interface so that it does not load the complete file into memory. The endpoint is files/Node-Android-Chat.zip.

The request is defined as

Call<ResponseBody> downloadFile();

Where ResponseBody is inbuilt class which is used to get the complete body of response.

RequestInterface.java

package com.learn2crack.filedownload;
 
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Streaming;
 
public interface RetrofitInterface {
 
    @GET("files/Node-Android-Chat.zip")
    @Streaming
    Call<ResponseBody> downloadFile();
}

Creating Service

Here we create DownloadService class which extends IntentService. If we download file using AsyncTask within Activity life cycle, the download will be interrupted when the device is rotated. In order to avoid that we perform download using IntentService and pass the result back to Activity using a Broadcast. Also we need not worry about threads while using Intent Service, it automatically creates a new thread to do work and destroys as soon as the work is finished.

We also use Notification progress to display how much file downloaded. The initDownload() method initializes the download and gets the ResponseBody object. Previously we used Retrofit to make Asynchronous requests. Since we are using IntentService we can make Synchronous request. It is done by using calling execute() method on Call object. For Asynchronous we used enqueue() method.

The ResponseBody object is passed to downloadFile() method which starts the download. The downloaded file is stored in Downloads directory. The total file size is obtained by calling contentLength() method on ResponseBody object which returns result in bytes. We download the file by passing the InputStream to BufferedInputStream. While downloading the file the notification is sent every 1 second so that it does not affect the main thread.

When the download is completed the onDownloadComplete() method is called. The sendIntent() method sends the broadcast using LocalBroadcastManager which we will handle in MainActivity. The sendNotification() method updates the notification progress.The onTaskRemoved method will be called when the app is destroyed completely.

DownloadService.java

package com.learn2crack.filedownload;
 
import android.app.IntentService;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
 
import com.learn2crack.filedownload.models.Download;
 
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
 
import okhttp3.ResponseBody;
 
import retrofit2.Call;
import retrofit2.Retrofit;
 
public class DownloadService extends IntentService {
 
    public DownloadService() {
        super("Download Service");
    }
 
    private NotificationCompat.Builder notificationBuilder;
    private NotificationManager notificationManager;
    private int totalFileSize;
 
    @Override
    protected void onHandleIntent(Intent intent) {
 
        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 
        notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_download)
                .setContentTitle("Download")
                .setContentText("Downloading File")
                .setAutoCancel(true);
        notificationManager.notify(0, notificationBuilder.build());
 
        initDownload();
 
    }
 
    private void initDownload(){
 
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://download.learn2crack.com/")
                .build();
 
        RetrofitInterface retrofitInterface = retrofit.create(RetrofitInterface.class);
 
        Call<ResponseBody> request = retrofitInterface.downloadFile();
        try {
 
            downloadFile(request.execute().body());
 
        } catch (IOException e) {
 
            e.printStackTrace();
            Toast.makeText(getApplicationContext(),e.getMessage(),Toast.LENGTH_SHORT).show();
 
        }
    }
 
    private void downloadFile(ResponseBody body) throws IOException {
 
        int count;
        byte data[] = new byte[1024 * 4];
        long fileSize = body.contentLength();
        InputStream bis = new BufferedInputStream(body.byteStream(), 1024 * 8);
        File outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "file.zip");
        OutputStream output = new FileOutputStream(outputFile);
        long total = 0;
        long startTime = System.currentTimeMillis();
        int timeCount = 1;
        while ((count = bis.read(data)) != -1) {
 
            total += count;
            totalFileSize = (int) (fileSize / (Math.pow(1024, 2)));
            double current = Math.round(total / (Math.pow(1024, 2)));
 
            int progress = (int) ((total * 100) / fileSize);
 
            long currentTime = System.currentTimeMillis() - startTime;
 
            Download download = new Download();
            download.setTotalFileSize(totalFileSize);
 
            if (currentTime > 1000 * timeCount) {
 
                download.setCurrentFileSize((int) current);
                download.setProgress(progress);
                sendNotification(download);
                timeCount++;
            }
 
            output.write(data, 0, count);
        }
        onDownloadComplete();
        output.flush();
        output.close();
        bis.close();
 
    }
 
    private void sendNotification(Download download){
 
        sendIntent(download);
        notificationBuilder.setProgress(100,download.getProgress(),false);
        notificationBuilder.setContentText("Downloading file "+ download.getCurrentFileSize() +"/"+totalFileSize +" MB");
        notificationManager.notify(0, notificationBuilder.build());
    }
 
    private void sendIntent(Download download){
 
        Intent intent = new Intent(MainActivity.MESSAGE_PROGRESS);
        intent.putExtra("download",download);
        LocalBroadcastManager.getInstance(DownloadService.this).sendBroadcast(intent);
    }
 
    private void onDownloadComplete(){
 
        Download download = new Download();
        download.setProgress(100);
        sendIntent(download);
 
        notificationManager.cancel(0);
        notificationBuilder.setProgress(0,0,false);
        notificationBuilder.setContentText("File Downloaded");
        notificationManager.notify(0, notificationBuilder.build());
 
    }
 
    @Override
    public void onTaskRemoved(Intent rootIntent) {
        notificationManager.cancel(0);
    }
 
}

Creating Activity

Butter Knife is used for View binding. The Broadcast Receiver is registered to receive progress from Service. The DownloadService is started when the Download button is pressed. Since we require write external storage permission we request the permission on run time before starting the service.

MainActivity

package com.learn2crack.filedownload;
 
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ProgressBar;
import android.widget.TextView;
 
import com.learn2crack.filedownload.models.Download;
 
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
 
public class MainActivity extends AppCompatActivity {
 
    public static final String MESSAGE_PROGRESS = "message_progress";
    private static final int PERMISSION_REQUEST_CODE = 1;
 
    @BindView(R.id.progress) ProgressBar mProgressBar;
    @BindView(R.id.progress_text) TextView mProgressText;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
 
        registerReceiver();
    }
 
    @OnClick(R.id.btn_download)
    public void downloadFile(){
 
        if(checkPermission()){
            startDownload();
        } else {
            requestPermission();
        }
    }
 
    private void startDownload(){
 
        Intent intent = new Intent(this,DownloadService.class);
        startService(intent);
 
    }
 
    private void registerReceiver(){
 
        LocalBroadcastManager bManager = LocalBroadcastManager.getInstance(this);
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(MESSAGE_PROGRESS);
        bManager.registerReceiver(broadcastReceiver, intentFilter);
 
    }
 
    private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
 
            if(intent.getAction().equals(MESSAGE_PROGRESS)){
 
                Download download = intent.getParcelableExtra("download");
                mProgressBar.setProgress(download.getProgress());
                if(download.getProgress() == 100){
 
                    mProgressText.setText("File Download Complete");
 
                } else {
 
                    mProgressText.setText(String.format("Downloaded (%d/%d) MB",download.getCurrentFileSize(),download.getTotalFileSize()));
 
                }
            }
        }
    };
 
    private boolean checkPermission(){
        int result = ContextCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if (result == PackageManager.PERMISSION_GRANTED){
 
            return true;
 
        } else {
 
            return false;
        }
    }
 
    private void requestPermission(){
 
        ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},PERMISSION_REQUEST_CODE);
 
    }
 
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_CODE:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 
                        startDownload();
                } else {
 
                    Snackbar.make(findViewById(R.id.coordinatorLayout),"Permission Denied, Please allow to proceed !", Snackbar.LENGTH_LONG).show();
 
                }
                break;
        }
    }
 
}

Adding Permissions, Service in Manifest

We need permission to access Internet and to write External Storage.

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

Also add the DownloadService to Manifest.

<service android:name=".DownloadService"/>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.learn2crack.filedownload">
 
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".DownloadService"/>
    </application>
 
</manifest>

Screenshots

You may also like...