安卓断点下载apk以及安装
安卓断点下载apk以及安装
前言
一般app内部都有更新功能,其实很多app下载都没有断点下载的功能,此功能比较复杂性价比不高,现在大家流量又多一般都是直接一次性下载完,要么就跳转应用市场和网页一劳永逸,毕竟人家浏览器和应用市场自带的断点下载功能他不香么。不过手撕一个功能还是比较有意思的毕竟也不难。
知识点
这个需要的知识点无非就是以下几个
- 普通下载
- 本地储存文件权限
- HTTP协议请求头(Range) 响应码206
- RandomAccessFile
- 数据库(不用greendao)对应下载文件关系,以及下载状态处理
- OKhttp和HttpURLConnection的区别
- 安装页面权限配置跳转
流程
- 通过content-length响应头获取文件的总长度
- 配合RandomAccessFile、响应码206、请求头range,以及安卓HttpURLConnection和Rxjava2.0实现下载功能
- 使用数据库管理类完善下载节点
普通下载
可以参考下面的文章,无非就是流的处理
https://blog.csdn.net/arios171/article/details/81127458
普通下载先提供一个java版本的代码,以下是javabean代码
普通下载中主要的知识点是content-length,获取这个请求头的方法有三种:
HttpURLConnection.getHeaderField("content-length");
HttpURLConnection.getContentLength();
HttpURLConnection.getContentLengthLong();
这里建议用HttpURLConnection.getHeaderField(“content-length”);获取到字符串之后自己去解析成long类型的,getContentLength默认是int类型如果下载大文件可能会超出范围,getContentLengthLong只有在min sdk android 24才能使用。
public class ProgressBean {
/**
* 名称
*/
private String fileName;
/**
* 路径
*/
private String path;
/**
* 进度条
*/
private long progress;
/**
* 大小
*/
private long size;
/**
* 类型
*/
private int type;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public long getProgress() {
return progress;
}
public void setProgress(long progress) {
this.progress = progress;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
下面是下载的代码,里面封装了Rxjava2的HttpURLConnection下载
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import io.reactivex.Observable;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.schedulers.Schedulers;
/**
* @author hskun20
*/
public class DownloadManager {
private final IHttpDownload listener;
public static final String TAG = "hskun";
public DownloadManager(@NonNull IHttpDownload listener) {
this.listener = listener;
}
public interface IHttpDownload {
/**
* 下载成功,主线程
*
* @param progressBean 进度条数据
*/
void onSuccess(ProgressBean progressBean);
/**
* 进度条,此方法在主线程中调用
*
* @param progressBean 进度条数据
*/
void onProgress(ProgressBean progressBean);
/**
* 进度条,此方法在IO线程中调用
*
* @param progressBean 进度条数据
*/
void onIOThreadProgress(ProgressBean progressBean);
/**
* 下载失败,此方法在主线程中调用
*
* @param error 错误信息
* @param type 下载分类标识
*/
void onError(Throwable error, int type);
/**
* 下载失败,此方法在computation线程中调用
*
* @param error 错误信息
* @param type 下载分类标识
*/
void onComputationThreadError(Throwable error, int type);
/**
* 下载操作结束,不管是失败还是成功,主线程
*
* @param type 下载类型标识
*/
void onHttpDown(int type);
}
//HTTP请求方式
public static final String GET = "GET";
public static final String POST = "POST";
public static final String HEAD = "HEAD";
public static final String OPTIONS = "OPTIONS";
public static final String PUT = "PUT";
public static final String DELETE = "DELETE";
public static final String TRACE = "TRACE";
public void downloadGet(String url, File file, int type) {
download(url, file, GET, type);
}
public void downloadPost(String url, File file, int type) {
download(url, file, POST, type);
}
/**
* @param url 下载的连接
* @param file 目标下载的位置
* @param method 请求方法
* @param type 请求的类型作为接口回调的标识
*/
public void download(final String url, final File file, final String method, final int type) {
Observable.create((ObservableOnSubscribe<ProgressBean>) emitter -> {
createFile(file);
URL mUrl = new URL(url);
mUrl.openConnection(null);
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
connection.connect();
InputStream bytes = connection.getInputStream();
String header = connection.getHeaderField("Content-Length");
int size = 0;
try {
size = Integer.parseInt(header);
} catch (NumberFormatException ignored) {
}
if (file.exists()) {
FileOutputStream fos = new FileOutputStream(file);
byte[] b = new byte[102400];
long progress = 0;
int len;
while ((len = bytes.read(b)) != -1) {
progress += len;
fos.write(b, 0, len);
ProgressBean progressBean = new ProgressBean();
progressBean.setFileName(file.getName());
progressBean.setPath(file.getAbsolutePath());
progressBean.setProgress(progress);
progressBean.setSize(size);
progressBean.setType(type);
emitter.onNext(progressBean);
//这里就是关键的实时更新进度了!
}
}
emitter.onComplete();
//整个异常出错会自动调用onError
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(progressBean -> {
Log.i(TAG, "onNext accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onProgress(progressBean);
}
})
.observeOn(Schedulers.io())
.doOnNext(progressBean -> {
Log.i(TAG, "onNext accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onIOThreadProgress(progressBean);
}
})
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> {
Log.i(TAG, "onError accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onError(throwable, type);
listener.onHttpDown(type);
}
})
.observeOn(Schedulers.computation())
.doOnError(throwable -> {
Log.i(TAG, "onError accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onComputationThreadError(throwable, type);
listener.onHttpDown(type);
}
})
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(() -> {
if (listener != null) {
ProgressBean progressBean = new ProgressBean();
progressBean.setFileName(file.getName());
progressBean.setPath(file.getAbsolutePath());
progressBean.setProgress(file.length());
progressBean.setSize(file.length());
progressBean.setType(type);
listener.onSuccess(progressBean);
listener.onHttpDown(type);
}
})
.subscribe();
}
/**
* 创建文件顺便创建父目录
*
* @param file file类
*/
public static void createFile(File file) {
if (file.exists() && file.isFile()) {
file.delete();
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
File parentFile = file.getParentFile();
if (parentFile.exists()) {
if (parentFile.isFile()) {
parentFile.delete();
parentFile.mkdirs();
}
} else {
parentFile.mkdirs();
}
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
HTTP相关请求头和响应码
以上内容比较通用的主要就是HTTP协议,HTTP协议默认实现了断点下载的功能。其中请求头为range,响应码为206
断点下载的时候,需要设置请求头的“Range”
- 表示头100个字节:Range:bytes=0-99
- 表示第二个100个字节:Range:bytes=100-199
- 表示最后100个字节:Range:bytes=-100
- 表示200个字节以后的所有字节:Range:bytes=200-
配合RandomAccessFile、响应码206、请求头range,以及安卓HttpURLConnection和Rxjava2.0实现下载功能
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* @author hskun20
*/
public class DownloadManager {
private IHttpDownload listener;
public static final String TAG = "hskun";
private final Context mContext;
public DownloadManager(Context context, IHttpDownload listener) {
this.mContext = context;
this.listener = listener;
}
/**
* 取消下载
*/
public void cancel() {
this.listener = null;
}
public abstract static class HttpDownLoad implements IHttpDownload {
@Override
public void onIOThreadProgress(ProgressBean progressBean) {
}
@Override
public void onError(Throwable error, int type) {
}
@Override
public void onComputationThreadError(Throwable error, int type) {
}
@Override
public void onHttpDown(int type) {
}
}
public interface IHttpDownload {
/**
* 下载成功,主线程
*
* @param progressBean 进度条数据
*/
void onSuccess(ProgressBean progressBean);
/**
* 进度条,此方法在主线程中调用
*
* @param progressBean 进度条数据
*/
void onProgress(ProgressBean progressBean);
/**
* 进度条,此方法在IO线程中调用
*
* @param progressBean 进度条数据
*/
void onIOThreadProgress(ProgressBean progressBean);
/**
* 下载失败,此方法在主线程中调用
*
* @param error 错误信息
* @param type 下载分类标识
*/
void onError(Throwable error, int type);
/**
* 下载失败,此方法在computation线程中调用
*
* @param error 错误信息
* @param type 下载分类标识
*/
void onComputationThreadError(Throwable error, int type);
/**
* 下载操作结束,不管是失败还是成功,主线程
*
* @param type 下载类型标识
*/
void onHttpDown(int type);
}
//HTTP请求方式
public static final String GET = "GET";
public static final String POST = "POST";
public static final String HEAD = "HEAD";
public static final String OPTIONS = "OPTIONS";
public static final String PUT = "PUT";
public static final String DELETE = "DELETE";
public static final String TRACE = "TRACE";
public void downloadGet(String url, File file, int type) {
download(url, file, GET, type);
}
public void downloadPost(String url, File file, int type) {
download(url, file, POST, type);
}
/**
* @param url 下载的连接
* @param file 目标下载的位置
* @param method 请求方法
* @param type 请求的类型作为接口回调的标识
*/
public void download(final String url, final File file, final String method, final int type) {
DownloadDbHelper helper = new DownloadDbHelper(mContext);
Observable<ProgressBean> observable = Observable.create(emitter -> {
ProgressBean progressBean = helper.findProgressByUrl(url);
if (progressBean == null) {//如果没有那么说明就没有这个ProgressBean
progressBean = new ProgressBean();
} else {//如果直接杀死进程状态来不及改
if (progressBean.getSize() != 0 && progressBean.getSize() == progressBean.getProgress()) {
emitter.onNext(progressBean);
emitter.onComplete();
return;
}
}
if (!file.exists() || !file.isFile())//如果这个文件不存在才重新创建
createFile(file);
URL mUrl = new URL(url);
long size = -1;
RandomAccessFile raf = new RandomAccessFile(file, "rwd");
if (progressBean.getSize() != 0) {
size = progressBean.getSize();
} else {
//拿到总长度
HttpURLConnection lengthConnection = (HttpURLConnection) mUrl.openConnection();
lengthConnection.setRequestMethod(method);
lengthConnection.setConnectTimeout(1000);
lengthConnection.setReadTimeout(1000);
lengthConnection.connect();
String header = lengthConnection.getHeaderField("content-length");
try {
size = Long.parseLong(header);
} catch (Exception ignored) {
}
raf.setLength(size);//设置长度,设置长度会破坏已下载的文件
}
//设置文件写入位置
raf.seek(progressBean.getProgress());//设置光标位置
//开始断点下载
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout(1000);
connection.setReadTimeout(1000);
connection.setRequestProperty("Range", "bytes=" + progressBean.getProgress() + "-" + size);//表示下载start以后的所有字节
// connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());//不做分片所以这里没有用
int responseCode = connection.getResponseCode();//这个方法可以替代正常下载的 connection.connect();方法做请求操作
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {//这边响应码必须等于206
InputStream inputStream = connection.getInputStream();
try {
if (file.exists()) {
byte[] b = new byte[1024 * 8];//len最大也就8192
long progress = progressBean.getProgress();
int len;
while ((len = inputStream.read(b)) != -1) {
progress += len;
raf.write(b, 0, len);
// raf.write(b, 0, len);
progressBean.setFileName(file.getName());
progressBean.setUrl(url);
progressBean.setIsRunning(1);
progressBean.setFinished(0);
progressBean.setPath(file.getAbsolutePath());
progressBean.setProgress(progress);
progressBean.setSize(size);
progressBean.setType(type);
//插入到进度条,取消订阅这里会继续下载,所以为了不浪费在Observable中继续操作不要在观察者中操作
helper.insertOrReplace(progressBean);
//回显进度
emitter.onNext(progressBean);
//这里就是关键的实时更新进度了!
}
}
emitter.onComplete();
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
connection.disconnect();
try {
if (raf != null)
raf.close();
inputStream.close();
} catch (IOException e) {
emitter.onError(e);
}
}
}
//整个异常出错会自动调用onError
});
Disposable subscribe = observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(progressBean -> {
Log.i(TAG, "onNext accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onProgress(progressBean);
}
})
.observeOn(Schedulers.io())
.doOnNext(progressBean -> {
Log.i(TAG, "onNext accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onIOThreadProgress(progressBean);
}
})
.observeOn(AndroidSchedulers.mainThread())
.doOnError(throwable -> {
Log.i(TAG, "onError accept " + Thread.currentThread().toString());
if (listener != null) {
listener.onError(throwable, type);
listener.onHttpDown(type);
}
})
.observeOn(Schedulers.computation())
.doOnError(throwable -> {
Log.i(TAG, "onError accept " + Thread.currentThread().toString());
if (listener != null) {
ProgressBean progressBean = new ProgressBean();
progressBean.setFileName(file.getName());
progressBean.setFinished(0);
progressBean.setUrl(url);
progressBean.setRunning(0);
progressBean.setPath(file.getAbsolutePath());
progressBean.setSize(file.length());
progressBean.setType(type);
helper.insertOrReplaceNoProgress(progressBean);
listener.onComputationThreadError(throwable, type);
listener.onHttpDown(type);
}
})
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(() -> {
if (listener != null) {
ProgressBean progressBean = new ProgressBean();
progressBean.setFileName(file.getName());
progressBean.setFinished(1);
progressBean.setUrl(url);
progressBean.setRunning(0);
progressBean.setPath(file.getAbsolutePath());
progressBean.setProgress(file.length());
progressBean.setSize(file.length());
progressBean.setType(type);
helper.insertOrReplace(progressBean);
listener.onSuccess(progressBean);
listener.onHttpDown(type);
Log.e(TAG, "OnComplete " + progressBean.toString());
}
})
.subscribe(progressBean -> {
}, throwable -> {
Log.e(TAG, throwable.getMessage(), throwable);
});
}
public static void installAPK(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 7.0+以上版本
Uri apkUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName(), file); //包名.fileprovider
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
context.startActivity(intent);
}
/**
* 创建文件顺便创建父目录
*
* @param file file类
*/
public static void createFile(File file) {
if (file.exists() && file.isFile()) {
file.delete();
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
File parentFile = file.getParentFile();
if (parentFile.exists()) {
if (parentFile.isFile()) {
parentFile.delete();
parentFile.mkdirs();
}
} else {
parentFile.mkdirs();
}
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
数据库处理
为什么不用greendao和其他的第三方库,因为这些写起来比较难移植,处理起来比较麻烦,原生的库可以很方便作为库依赖到别的项目里面
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DownloadDbHelper extends SQLiteOpenHelper {
/**
* 备注如下,主键唯一值是url
*/
private final static String DOWNLOAD_INFO = "CREATE TABLE download_info(" +
"url TEXT PRIMARY KEY NOT NULL," +//文件网络路径
"file_name TEXT," +//文件名称
"path TEXT," +//文件路径
"finished INTEGER," +//是否下载完成0否1是
"progress INTEGER," +//进度条
"is_running INTEGER," +//是否下载中0否1是
"size INTEGER" +//文件大小
")";
/**
* 下载列名称
*/
private final static String[] DOWNLOAD_COLUMN = {
"url",
"file_name",
"path",
"finished",
"progress",
"is_running",
"size"
};
public DownloadDbHelper(@Nullable Context context) {
super(context, "download.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
createAllTable(db);
createIndex(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (newVersion > oldVersion) {
clearAll(db);
}
}
private void clearAll(SQLiteDatabase db) {
db.beginTransaction();
try {
dropAllTable(db);
createAllTable(db);
createIndex(db);
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
db.endTransaction();
}
}
private void createIndex(SQLiteDatabase db) {
db.execSQL("CREATE INDEX url ON download_info(url)");
}
/**
* 这里增加两个索引,一个是id另一个是网络路径
*/
private void dropAllTable(SQLiteDatabase db) {
db.execSQL("DROP TABLE download_info");
}
private void createAllTable(SQLiteDatabase db) {
db.execSQL(DOWNLOAD_INFO);
}
/**
* 搜索对应的链接下载信息,如果没有查到就给null
*
* @param url 下载链接
*/
public ProgressBean findProgressByUrl(String url) {
SQLiteDatabase db = getWritableDatabase();
String selection = "url = ?";
String[] selectionArgs = {url};
try {
Cursor cursor = db.query(
"download_info",
DOWNLOAD_COLUMN,
selection,
selectionArgs,
null,
null,
null,
null
);
Map<String, Object> map = queryMap(cursor, DOWNLOAD_COLUMN);
if (map != null) {
ProgressBean progressBean = new ProgressBean();
progressBean.setFileName((String) map.get("file_name"));
progressBean.setPath((String) map.get("path"));
progressBean.setUrl((String) map.get("url"));
progressBean.setFinished((Long) map.get("finished"));
progressBean.setProgress((Long) map.get("progress"));
progressBean.setRunning((Long) map.get("is_running"));
progressBean.setSize((Long) map.get("size"));
return progressBean;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 插入一条下载信息
*/
public void insertOrReplace(ProgressBean progressBean) {
if (TextUtils.isEmpty(progressBean.getUrl())) {//如果主键url是空的那么就不做操作
return;
}
SQLiteDatabase db = getWritableDatabase();
String sql = "INSERT OR REPLACE INTO download_info (url,file_name,path,finished,progress,is_running,size)" +
"VALUES ('" + progressBean.getUrl() + "','" +
progressBean.getFileName() +
"','" +
progressBean.getPath() +
"'," +
progressBean.getFinished() +
"," +
progressBean.getProgress() +
"," +
progressBean.getIsRunning() +
"," +
progressBean.getSize() +
")";
db.execSQL(sql);
}
/**
* 错误的时候插入只更新除了进度的状态
*/
public void insertOrReplaceNoProgress(ProgressBean progressBean) {
if (TextUtils.isEmpty(progressBean.getUrl())) {//如果主键url是空的那么就不做操作
return;
}
SQLiteDatabase db = getWritableDatabase();
String sql = "INSERT OR REPLACE INTO download_info (url,file_name,path,finished,is_running,size)" +
"VALUES ('" + progressBean.getUrl() + "','" +
progressBean.getFileName() +
"','" +
progressBean.getPath() +
"'," +
progressBean.getFinished() +
"," +
progressBean.getIsRunning() +
"," +
progressBean.getSize() +
")";
db.execSQL(sql);
}
/**
* 设置好cursor然后直接将cursor转成list
*
* @param cursor 设置好的cursor里面包含了查询条件
* @return 返回list集合
*/
private List<Map<String, Object>> queryList(Cursor cursor,
String[] names) {
String[] columNames = names;
List<Map<String, Object>> list = new ArrayList<>();
while (cursor.moveToNext()) {
String[] columnNames = cursor.getColumnNames();
if (columNames == null || columNames.length == 0 || columNames.length > columnNames.length) {//如果是空的就直接用cursor的列
columNames = columnNames;
}
Map<String, Object> map = new HashMap<>();
for (String name : columNames) {
int columnIndex = cursor.getColumnIndex(name);
switch (cursor.getType(columnIndex)) {
case Cursor.FIELD_TYPE_INTEGER:
map.put(name, cursor.getLong(columnIndex));
break;
case Cursor.FIELD_TYPE_STRING:
map.put(name, cursor.getString(columnIndex));
break;
case Cursor.FIELD_TYPE_FLOAT:
map.put(name, cursor.getFloat(columnIndex));
break;
case Cursor.FIELD_TYPE_NULL:
map.put(name, null);
break;
}
}
list.add(map);
}
cursor.close();
return list;
}
/**
* 设置好cursor然后直接将cursor转成map只解析一行
*
* @param cursor 设置好的cursor里面包含了查询条件
* @return 返回list集合
*/
private Map<String, Object> queryMap(Cursor cursor, String[] names) {
String[] columNames = names;
String[] columnNames = cursor.getColumnNames();
if (columNames == null || columNames.length == 0 || columNames.length > columnNames.length) {//如果是空的就直接用cursor的列
columNames = columnNames;
}
if (!cursor.moveToFirst()) { //没有查到一个直接返回null
return null;
}
Map<String, Object> map = new HashMap<>();
for (String name : columNames) {
int columnIndex = cursor.getColumnIndex(name);
switch (cursor.getType(columnIndex)) {
case Cursor.FIELD_TYPE_INTEGER:
map.put(name, cursor.getLong(columnIndex));
break;
case Cursor.FIELD_TYPE_STRING:
map.put(name, cursor.getString(columnIndex));
break;
case Cursor.FIELD_TYPE_FLOAT:
map.put(name, cursor.getFloat(columnIndex));
break;
case Cursor.FIELD_TYPE_NULL:
map.put(name, null);
break;
}
}
cursor.close();
return map;
}
}
ProgressBean
progressBean中url设置为主键,type只为请求的时候使用
public class ProgressBean {
/**
* 名称
*/
private String fileName;
/**
* 路径
*/
private String path;
/**
* 网络路径
*/
private String url;
/**
* 是否完成进度100%并不能代表完成,实际文件长度和contentLength不一定一样
*/
private long finished;
/**
* 是否下载中
*/
private long isRunning;
/**
* 进度条,作用很多判断从哪里下载,实际的字节数progress,单位对应contentLength
*/
private long progress;
/**
* 大小,实际的字节数size,单位对应contentLength
*/
private long size;
/**
* 类型
*/
private int type;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public long getProgress() {
return progress;
}
public void setProgress(long progress) {
this.progress = progress;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public void setFinished(long finished) {
this.finished = finished;
}
public long getFinished() {
return finished;
}
public long getIsRunning() {
return isRunning;
}
public void setIsRunning(long isRunning) {
this.isRunning = isRunning;
}
public void setRunning(long running) {
isRunning = running;
}
@Override
public String toString() {
return "ProgressBean{" +
", fileName='" + fileName + '\'' +
", path='" + path + '\'' +
", url='" + url + '\'' +
", finished=" + finished +
", isRunning=" + isRunning +
", progress=" + progress +
", size=" + size +
", type=" + type +
'}';
}
}
OKhttp和HttpURLConnection的区别
OKhttp和HttpURLConnection的区别,为啥不用OKhttp,因为我使用OKhttp的过程中发现默认OKhttp是直接把流全部读完了再给我们使用的,大文件下载的时候进入会一直是0,如果下载完成会突然拉满进度。如果要使用OKhttp需要使用ok的内核Okio中的流来做下载,不然的话Okhttp会帮你下载完成了之后才把流返回给你。不得不说okhttp的缓存确实挺不错,但是用在下载大文件的时候这个策略成了鸡肋。
下载调用
下载调用主要是用到了如下方法,主要是初始化DownloadManager,然后在确定独写权限之后下载即可,可用downloadGet和downloadPost,如果有参数可以修改上面的下载类。
private DownloadManager manager;
private void initData() {
manager = new DownloadManager(this, new DownloadManager.HttpDownLoad() {
@Override
public void onSuccess(ProgressBean progressBean) {
DownloadManager.installAPK(UpdateDialogActivity.this, new File(progressBean.getPath()));
}
@Override
public void onProgress(ProgressBean progressBean) {
long progress = progressBean.getProgress();
double size = (double) progressBean.getSize();
int percentProgress = (int) (progress / size * 100f);
bp.setProgress(percentProgress);
int progressMB = (int) (progress / 1024f / 1024f);
int sizeMB = (int) (size / 1024f / 1024f);
ratio.setText(percentProgress + "%" + progressMB + "MB/" + sizeMB + "MB");
}
});
startDownload();
}
/**
* 下载apk文件
*/
private void startDownload() {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
manager.downloadGet(
"https://gyxz2.243ty.com/com.iflytek.iflyapp.apk",
new File(Environment.getExternalStorageDirectory() + "/xxx/download", "xxx.apk"),
0
);
} else {
View view = getWindow().getDecorView();
Snackbar.make(
view,
"没有检查到sd卡无法下载",
Snackbar.LENGTH_SHORT
).show();
}
}
权限管理
权限管理用的是XXPermissions,可以自行在github搜索,示例代码如下
com.hjq.permissions.XXPermissions.with((Activity) mContext)
.permission(Permission.Group.STORAGE)
.request(new OnPermission() {
@Override
public void hasPermission(List<String> granted, boolean isAll) {
if (isAll) {
Intent intent = new Intent(mContext, XXXActivity.class);
startActivity(intent);
}
}
@Override
public void noPermission(List<String> denied, boolean quick) {
showToastTop("请授权之后再使用此功能");
}
});
最后就是安装
基于7.0以上安装需要做一些操作,代码如下,需要写一个xml文件放在res xml文件夹下
public static void installAPK(Context context, File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 7.0+以上版本
Uri apkUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName(), file); //包名.fileprovider
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
context.startActivity(intent);
}
manifest需要配置如下,需要注意10以上需要android:requestLegacyExternalStorage=“true”,因为10以上写权限有很大变化,这里直接跳过用原来的老权限方式,配置provider则如下就可以
<application
android:requestLegacyExternalStorage="true" >
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
</provider>
</application>
@xml/paths文件如下,这样可以存放任意apk到任意sd卡位置使用
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
<external-path path="path" name="download"/>
<!--<external-path name="external_path" path=""/>-->
</paths>