美文网首页优秀案例Android应用
Android基于rxjava2+retrofit2实现断点续传

Android基于rxjava2+retrofit2实现断点续传

作者: Zwww_ | 来源:发表于2018-03-08 15:43 被阅读2529次

    前言

    在rxjava和retrofit日益火热的今天,我们也要给自己定个小目标,比如说利用其来实现支付宝更新app的断点续传下载功能。

    基本原理

    其实下载文件就是一个get请求,而断点续传则是要把发生异常时,已经下载的位置记录下来,再次下载时从这个位置继续下载。此时就要涉及到两个知识点了,一个Range的请求头字段(有了这个字段就可以读取服务端该文件的字节范围,从而实现从断点处继续下载)。一个RandomAccessFile类,这个类可以通过seek方法设置指针位置,从而来实现从断点处继续写入文件。

    效果图

    效果图 .gif

    代码实现

    1、添加依赖

    implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    

    2、核心代码

    RetrofitHttp

    基于Retrofit封装的网络请求类,大致流程就是创建一个RetrofitHttp单例,内部使用Retrofit来实现网络请求,执行下载时,可以先创建一个本app默认的文件下载目录,先拿到需要下载文件的下载内容范围然后执行请求,对响应体再进行文件指定位置的写入,同时反馈下载的进度和用sp存储已下载的字节数。这里注意:RandomAccessFile以rwd模式打开时,如果文件不存在,则会创建这个文件。由于是在IntentService的工作线程里调用下载,所以这里不进行线程切换。

    public class RetrofitHttp {
        private static final int DEFAULT_TIMEOUT = 10;
        private static final String TAG = "RetrofitClient";
    
        private ApiService apiService;
    
        private OkHttpClient okHttpClient;
    
        public static String baseUrl = ApiService.BASE_URL;
    
        private static RetrofitHttp sIsntance;
    
        public static RetrofitHttp getInstance() {
            if (sIsntance == null) {
                synchronized (RetrofitHttp.class) {
                    if (sIsntance == null) {
                        sIsntance = new RetrofitHttp();
                    }
                }
            }
            return sIsntance;
        }
    
        private RetrofitHttp() {
            okHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                    .build();
            Retrofit retrofit = new Retrofit.Builder()
                    .client(okHttpClient)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .baseUrl(baseUrl)
                    .build();
            apiService = retrofit.create(ApiService.class);
        }
    
        public void downloadFile(final long range, final String url, final String fileName, final DownloadCallBack downloadCallback) {
            //断点续传时请求的总长度
            File file = new File(Constant.APP_ROOT_PATH + Constant.DOWNLOAD_DIR, fileName);
            String totalLength = "-";
            if (file.exists()) {
                totalLength += file.length();
            }
    
            apiService.executeDownload("bytes=" + Long.toString(range) + totalLength, url)
                    .subscribe(new Observer<ResponseBody>() {
                        @Override
                        public void onSubscribe(Disposable d) {
    
                        }
    
                        @Override
                        public void onNext(ResponseBody responseBody) {
                            RandomAccessFile randomAccessFile = null;
                            InputStream inputStream = null;
                            long total = range;
                            long responseLength = 0;
                            try {
                                byte[] buf = new byte[2048];
                                int len = 0;
                                responseLength = responseBody.contentLength();
                                inputStream = responseBody.byteStream();
                                String filePath = Constant.APP_ROOT_PATH + Constant.DOWNLOAD_DIR;
                                File file = new File(filePath, fileName);
                                File dir = new File(filePath);
                                if (!dir.exists()) {
                                    dir.mkdirs();
                                }
                                randomAccessFile = new RandomAccessFile(file, "rwd");
                                if (range == 0) {
                                    randomAccessFile.setLength(responseLength);
                                }
                                randomAccessFile.seek(range);
    
                                int progress = 0;
                                int lastProgress = 0;
    
                              while ((len = inputStream.read(buf)) != -1) {
                                    randomAccessFile.write(buf, 0, len);
                                    total += len;
                                    lastProgress = progress;
                                    progress = (int) (total * 100 / randomAccessFile.length());
                                    if (progress > 0 && progress != lastProgress) {
                                        downloadCallback.onProgress(progress);
                                    }
                                }
                                downloadCallback.onCompleted();
                            } catch (Exception e) {
                                Log.d(TAG, e.getMessage());
                                downloadCallback.onError(e.getMessage());
                                e.printStackTrace();
                            } finally {
                                try {
                                    SPDownloadUtil.getInstance().save(url, total);
                                    if (randomAccessFile != null) {
                                        randomAccessFile.close();
                                    }
    
                                    if (inputStream != null) {
                                        inputStream.close();
                                    }
    
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
    
                            }
                        }
    
                        @Override
                        public void onError(Throwable e) {
                            downloadCallback.onError(e.toString());
                        }
    
                        @Override
                        public void onComplete() {
                        }
                    });
        }
    }
    
    ApiService

    请求网络的API接口类,@Streaming注解是为了避免将整个文件读进内存,这是在下载大文件时需要注意的地方。在请求头添加Range就可以实现服务器文件的下载内容范围了。至于BASE_URL,这里演示的是下载腾讯视频,所以就用腾讯的了。

    public interface ApiService {
        public static final String BASE_URL = "http://dldir1.qq.com";
        @Streaming
        @GET
        Observable<ResponseBody> executeDownload(@Header("Range") String range, @Url() String url);
    }
    
    DownloadIntentService

    后台执行下载的服务,当下载任务完成时调取安装方法进行安装,服务会自己结束。优先会判断要下载的文件是否已经下载完成,如果已下载完,则直接安装。下载的进度会以通知的形式发送出来,这个是仿支付宝的,下载完成和发生异常时自动取消通知。

    public class DownloadIntentService extends IntentService {
    
        private static final String TAG = "DownloadIntentService";
        private NotificationManager mNotifyManager;
        private String mDownloadFileName;
        private Notification mNotification;
    
        public DownloadIntentService() {
            super("InitializeService");
        }
    
        @Override
        protected void onHandleIntent(@Nullable Intent intent) {
            String downloadUrl = intent.getExtras().getString("download_url");
            final int downloadId = intent.getExtras().getInt("download_id");
            mDownloadFileName = intent.getExtras().getString("download_file");
    
            Log.d(TAG, "download_url --" + downloadUrl);
            Log.d(TAG, "download_file --" + mDownloadFileName);
    
            final File file = new File(Constant.APP_ROOT_PATH + Constant.DOWNLOAD_DIR + mDownloadFileName);
            long range = 0;
            int progress = 0;
            if (file.exists()) {
                range = SPDownloadUtil.getInstance().get(downloadUrl, 0);
                progress = (int) (range * 100 / file.length());
                if (range == file.length()) {
                    installApp(file);
                    return;
                }
            }
    
            Log.d(TAG, "range = " + range);
    
            final RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notify_download);
            remoteViews.setProgressBar(R.id.pb_progress, 100, progress, false);
            remoteViews.setTextViewText(R.id.tv_progress, "已下载" + progress + "%");
    
            final NotificationCompat.Builder builder =
                    new NotificationCompat.Builder(this)
                            .setContent(remoteViews)
                            .setTicker("正在下载")
                            .setSmallIcon(R.mipmap.ic_launcher);
    
            mNotification = builder.build();
    
            mNotifyManager = (NotificationManager)
                    getSystemService(Context.NOTIFICATION_SERVICE);
            mNotifyManager.notify(downloadId, mNotification);
    
            RetrofitHttp.getInstance().downloadFile(range, downloadUrl, mDownloadFileName, new DownloadCallBack() {
                @Override
                public void onProgress(int progress) {
                    remoteViews.setProgressBar(R.id.pb_progress, 100, progress, false);
                    remoteViews.setTextViewText(R.id.tv_progress, "已下载" + progress + "%");
                    mNotifyManager.notify(downloadId, mNotification);
                }
    
                @Override
                public void onCompleted() {
                    Log.d(TAG, "下载完成");
                    mNotifyManager.cancel(downloadId);
                    installApp(file);
                }
    
                @Override
                public void onError(String msg) {
                    mNotifyManager.cancel(downloadId);
                    Log.d(TAG, "下载发生错误--" + msg);
                }
            });
        }
    
        private void installApp(File file) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
            startActivity(intent);
        }
    
    }
    
    MainActivity

    入口activity,只有一个下载按钮,执行下载前判断下载服务是否已运行

    public class MainActivity extends AppCompatActivity {
        private static final int DOWNLOADAPK_ID = 10;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            TextView tvDownload = findViewById(R.id.tv_download);
            tvDownload.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (isServiceRunning(DownloadIntentService.class.getName())) {
                        Toast.makeText(MainActivity.this, "正在下载", Toast.LENGTH_SHORT).show();
                        return;
                    }
                    //String downloadUrl = http://sqdd.myapp.com/myapp/qqteam/tim/down/tim.apk;
                    String downloadUrl = "/qqmi/aphone_p2p/TencentVideo_V6.0.0.14297_848.apk";
                    Intent intent = new Intent(MainActivity.this, DownloadIntentService.class);
                    Bundle bundle = new Bundle();
                    bundle.putString("download_url", downloadUrl);
                    bundle.putInt("download_id", DOWNLOADAPK_ID);
                    bundle.putString("download_file", downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1));
                    intent.putExtras(bundle);
                    startService(intent);
                }
            });
        }
    
        /**
         * 用来判断服务是否运行.
         *
         * @param className 判断的服务名字
         * @return true 在运行 false 不在运行
         */
        private boolean isServiceRunning(String className) {
    
            boolean isRunning = false;
            ActivityManager activityManager = (ActivityManager) this
                    .getSystemService(Context.ACTIVITY_SERVICE);
            List<ActivityManager.RunningServiceInfo> serviceList = activityManager
                    .getRunningServices(Integer.MAX_VALUE);
            if (!(serviceList.size() > 0)) {
                return false;
            }
            for (int i = 0; i < serviceList.size(); i++) {
                if (serviceList.get(i).service.getClassName().equals(className) == true) {
                    isRunning = true;
                    break;
                }
            }
            return isRunning;
        }
    }
    
    SPDownloadUtil

    记录下载位置的sp工具类

    public class SPDownloadUtil {
    
        private static SharedPreferences mSharedPreferences;
        private static SPDownloadUtil instance;
    
        private SPDownloadUtil() {
        }
    
        public static SPDownloadUtil getInstance() {
            if (mSharedPreferences == null || instance == null) {
                synchronized (SPDownloadUtil.class) {
                    if (mSharedPreferences == null || instance == null) {
                        instance = new SPDownloadUtil();
                        mSharedPreferences = MainApplication.getInstance().getSharedPreferences(MainApplication.getInstance().getPackageName() + ".downloadSp", Context.MODE_PRIVATE);
                    }
                }
            }
            return instance;
        }
    
        /**
         * 清空数据
         *
         * @return true 成功
         */
        public boolean clear() {
            return mSharedPreferences.edit().clear().commit();
        }
    
        /**
         * 保存数据
         *
         * @param key   键
         * @param value 保存的value
         */
        public boolean save(String key, long value) {
            return mSharedPreferences.edit().putLong(key, value).commit();
        }
    
        /**
         * 获取保存的数据
         *
         * @param key      键
         * @param defValue 默认返回的value
         * @return value
         */
        public long get(String key, long defValue) {
            return mSharedPreferences.getLong(key, defValue);
        }
    
    }
    
    DownloadCallBack

    下载回调接口

    public interface DownloadCallBack {
    
        void onProgress(int progress);
    
        void onCompleted();
    
        void onError(String msg);
    
    }
    
    Constant

    常量类,定义了根目录和下载文件的存放目录

    public class Constant {
        public final static String APP_ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + MainApplication.getInstance().getPackageName();
        public final static String DOWNLOAD_DIR = "/downlaod/";
    }
    
    activity_main.xml

    首页布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.damon.download.actvity.MainActivity">
    
        <TextView
            android:id="@+id/tv_download"
            android:layout_width="80dp"
            android:layout_height="40dp"
            android:background="@color/colorPrimaryDark"
            android:gravity="center"
            android:text="下载!"
            android:textColor="#ffffff"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>
    
    </android.support.constraint.ConstraintLayout>
    
    notify_download.xml

    通知布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#999999"
        android:gravity="center"
        android:orientation="horizontal"
        android:paddingLeft="15dp"
        android:paddingRight="10dp">
        <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:scaleType="fitCenter"
            android:src="@mipmap/ic_launcher"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginLeft="12dp"
            android:gravity="center"
            android:orientation="vertical">
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="我的应用"
                android:textColor="#333333"
                android:textSize="16sp"
                />
            <ProgressBar
                android:id="@+id/pb_progress"
                style="@style/Widget.AppCompat.ProgressBar.Horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:max="100"
                android:progress="0"
                android:progressTint="@color/colorPrimary"
                />
            <TextView
                android:layout_gravity="left"
                android:id="@+id/tv_progress"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="已下载"
                android:textColor="#333333"
                />
        </LinearLayout>
    </LinearLayout>
    

    github地址

    Zwww_的Github地址

    相关文章

      网友评论

        本文标题:Android基于rxjava2+retrofit2实现断点续传

        本文链接:https://www.haomeiwen.com/subject/jkfsfftx.html