DownloadManager是Android提供的用于下载的类,使用起来比较简单,它包含两个静态内部类DownloadManager.Query和DownloadManager.Request;
DownloadManager.Request用来请求一个下载,DownloadManager.Query用来查询下载信息
DownloadManager对象属于系统服务,通过getSystemService来进行安装
DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
一般获取完成后会变成全局变量,方便之后使用
在使用DownloadManager进行下载的时候,就会用到DownloadManager.Request
//使用DownLoadManager来下载
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//将文件下载到自己的Download文件夹下,必须是External的
//这是DownloadManager的限制
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
request.setDestinationUri(Uri.fromFile(file));
//添加请求 开始下载
long downloadId = mDownloadManager.enqueue(request);
首先会创建出一个DownloadManager.Request对象,在构造方法中接收Uri,其实就是下载地址,
然后是文件的存放路径,这里需要说明,DownloadManager下载的位置是不能放到内置存贮位置的,必须放到Enviroment中,这里建议放到自己应用的文件夹,不要直接放到SD卡中,也就是通过getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)获取到的路径,该位置的文件是属于应用自己的,在应用卸载时也会随着应用一起被删除掉,并且在使用该文件夹的时候,是不需要SD卡读写权限的
然后通过request.setDestinationUri来设置存储位置,最后将请求加入到downloadManager中,会获得一个downloadID,这个downloadID比较重要,之后下载状态,进度的查询都靠这个downloadID
在查询下载进度的时候,会通过downloadId来指定查询某一任务的具体进度
/**
* 获取进度信息
* @param downloadId 要获取下载的id
* @return 进度信息 max-100
*/
public int getProgress(long downloadId) {
//查询进度
DownloadManager.Query query = new DownloadManager.Query()
.setFilterById(downloadId);
Cursor cursor = null;
int progress = 0;
try {
cursor = mDownloadManager.query(query);//获得游标
if (cursor != null && cursor.moveToFirst()) {
//当前的下载量
int downloadSoFar = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
//文件总大小
int totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
progress = (int) (downloadSoFar * 1.0f / totalBytes * 100);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return progress;
}
在查询进度的时候会使用到DownloadManager.Query这个类,在查询的时候,也是使用的Cursor,跟查询数据库是一样的,进度信息会需要拿到文件的总大小,和当前大小,自己算一下,最后Cursor对象在使用过后不要忘记关闭了
下载完成后,DownloadManager会发送一个广播,并且会包含downloadId的信息
//下载完成的广播
private class DownloadFinishReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
//下载完成的广播接收者
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
}
}
注册这个广播接收者
//注册下载完成的广播
mReceiver = new DownloadFinishReceiver();
registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
这里需要注意一点,在下载完成后需要提升一下文件的读写权限,否则在安装的时候会出现apk解析失败的页面,就是别人访问不了我们的apk文件
/**
* 提升读写权限
* @param filePath 文件路径
* @return
* @throws IOException
*/
public static void setPermission(String filePath) {
String command = "chmod " + "777" + " " + filePath;
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec(command);
} catch (IOException e) {
e.printStackTrace();
}
}
chmod 是Linux下设置文件权限的命令,后面的三个数字每一个代表不同的用户组
权限分为三种:读(r=4),写(w=2),执行(x=1)
那么这三种权限就可以组成7种不同的权限,分别用1-7这几个数字代表,例如7 = 4 + 2 + 1,那么就代表该组用户拥有可读,可写,可执行的权限;5 = 4 + 1,就代表可读可执行权限
而三位数字就带包,该登陆用户,它所在的组,以及其他人
在7.0之前安装的时候,只需要通过隐式Intent来跳转,并且指定安装的文件Uri即可
Intent intent = new Intent(Intent.ACTION_VIEW);
// 由于没有在Activity环境下启动Activity,设置下面的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");context.startActivity(intent);
在Android7.0之后的版本运行上述代码会出现 android.os.FileUriExposedException
“私有目录被限制访问”是指在Android7.0中为了提高私有文件的安全性,面向 Android N 或更高版本的应用私有目录将被限制访问。
而7.0的” StrictMode API 政策” 是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。
之前代码用到的Uri.fromFile就是商城一个file://的Uri
在7.0之后,我们需要使用FileProvider来解决
第一步:
在AndroidManifest.xml清单文件中注册provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.chenfengyao.installapkdemo"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
provider>
需要注意一下几点:
1. exported:必须为false
2. grantUriPermissions:true,表示授予 URI 临时访问权限。
3. authorities 组件标识,都以包名开头,避免和其它应用发生冲突。
第二步:
指定共享文件的目录,需要在res文件夹中新建xml目录,并且创建file_paths
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<external-path path="" name="download"/>
paths>
resources>
path=”“,是有特殊意义的,它代表根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了。
第三部:
使用FileProvider
Intent intent = new Intent(Intent.ACTION_VIEW);
File file = (new File(apkPath));
// 由于没有在Activity环境下启动Activity,设置下面的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致 参数3 共享的文件
Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
context.startActivity(intent);
相较于之前的代码,会把Uri改成使用FiliProvider创建的Uri,并且添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)来对目标应用临时授权该Uri所代表的文件,而且getUriForFile中的authority参数需要填写清单文件中的authorities的值
兼容7.0的安装代码是不能在7.0之前的版本运行的,这个时候就需要进行版本的判断了
//普通安装
private static void installNormal(Context context,String apkPath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
//版本在7.0以上是不能直接通过uri访问的
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
File file = (new File(apkPath));
// 由于没有在Activity环境下启动Activity,设置下面的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致 参数3 共享的文件
Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");
}
context.startActivity(intent);
}
如果应用已经获取了root权限了,那么我们可以实现自动安装,即不会出现应用安装的页面,会在后台自己慢慢的安装,这个时候使用的就是用代码去写命令行了
/**
* 应用程序运行命令获取 Root权限,设备必须已破解(获得ROOT权限)
*
* @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath(); RootCommand(apkRoot);
* @return 0 命令执行成功
*/
public static int RootCommand(String command) {
Process process = null;
DataOutputStream os = null;
try {
process = Runtime.getRuntime().exec("su");
os = new DataOutputStream(process.getOutputStream());
os.writeBytes(command + "\n");
os.writeBytes("exit\n");
os.flush();
int i = process.waitFor();
Log.d("SystemManager", "i:" + i);
return i;
} catch (Exception e) {
Log.d("SystemManager", e.getMessage());
return -1;
} finally {
try {
if (os != null) {
os.close();
}
process.destroy();
} catch (Exception e) {
}
}
}
这个方法就是将命令写入到手机的shell中,su就代表root权限了,而命令执行成功的话,会返回0的,接下来是安装命令
String command = "pm install -r " + mApkPath;
-r 代表强制安装,否则如果手机中已有该应用的话就会安装失败了,值得注意的是,要想等待命令执行的结果这个过程是很漫长的,所以在使用命令的时候是需要放到主线程中的
在写完整代码的时候需要把下载的代码写到Service中,否则你的downloadid就得通过别的方式去存储了,而查询下载进度,也是需要一直去查了,那么就需要写一个循环,并且放到子线程中,我们用RxJava做会比较舒服
package com.example.chenfengyao.installapkdemo.utils;
import android.content.Context;
import android.os.Environment;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
/**
* Created by 陈丰尧 on 2017/4/16.
*/
public class IOUtils {
public static void closeIO(Closeable... closeables) {
if (closeables != null) {
for (Closeable closeable : closeables) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 删除之前的apk
*
* @param apkName apk名字
* @return
*/
public static File clearApk(Context context, String apkName) {
File apkFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), apkName);
if (apkFile.exists()) {
apkFile.delete();
}
return apkFile;
}
}
这里面主要用到了删除之前apk的代码,下载前如果有历史版本,就把它删掉,下载新的
package com.example.chenfengyao.installapkdemo.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.widget.Toast;
import java.io.File;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
/**
* If there is no bug, then it is created by ChenFengYao on 2017/4/19,
* otherwise, I do not know who create it either.
*/
public class InstallUtil {
/**
*
* @param context
* @param apkPath 要安装的APK
* @param rootMode 是否是Root模式
*/
public static void install(Context context, String apkPath,boolean rootMode){
if (rootMode){
installRoot(context,apkPath);
}else {
installNormal(context,apkPath);
}
}
/**
* 通过非Root模式安装
* @param context
* @param apkPath
*/
public static void install(Context context,String apkPath){
install(context,apkPath,false);
}
//普通安装
private static void installNormal(Context context,String apkPath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
//版本在7.0以上是不能直接通过uri访问的
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
File file = (new File(apkPath));
// 由于没有在Activity环境下启动Activity,设置下面的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致 参数3 共享的文件
Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");
}
context.startActivity(intent);
}
//通过Root方式安装
private static void installRoot(Context context, String apkPath) {
Observable.just(apkPath)
.map(mApkPath -> "pm install -r " + mApkPath)
.map(SystemManager::RootCommand)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(integer -> {
if (integer == 0) {
Toast.makeText(context, "安装成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "root权限获取失败,尝试普通安装", Toast.LENGTH_SHORT).show();
install(context,apkPath);
}
});
}
}
该类只负责安装APK,如果是Root模式的话,会首先进行尝试,如果失败了,还会调用一次普通模式,进行安装的,注意root模式安装的代码,不要忘记放到子线程中去执行了
package com.example.chenfengyao.installapkdemo.utils;
import android.util.Log;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* Created by 陈丰尧 on 2017/4/16.
*/
public class SystemManager {
/**
* 应用程序运行命令获取 Root权限,设备必须已破解(获得ROOT权限)
*
* @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath();
* @return 0 命令执行成功
*/
public static int RootCommand(String command) {
Process process = null;
DataOutputStream os = null;
try {
process = Runtime.getRuntime().exec("su");
os = new DataOutputStream(process.getOutputStream());
os.writeBytes(command + "\n");
os.writeBytes("exit\n");
os.flush();
int i = process.waitFor();
Log.d("SystemManager", "i:" + i);
return i;
} catch (Exception e) {
Log.d("SystemManager", e.getMessage());
return -1;
} finally {
try {
if (os != null) {
os.close();
}
process.destroy();
} catch (Exception e) {
}
}
}
/**
* 提升读写权限
* @param filePath 文件路径
* @return
* @throws IOException
*/
public static void setPermission(String filePath) {
String command = "chmod " + "777" + " " + filePath;
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec(command);
} catch (IOException e) {
e.printStackTrace();
}
}
}
该类主要就是放一些需要写入到shell中的代码
package com.example.chenfengyao.installapkdemo;
import android.app.DownloadManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.LongSparseArray;
import com.example.chenfengyao.installapkdemo.utils.IOUtils;
import com.example.chenfengyao.installapkdemo.utils.InstallUtil;
import com.example.chenfengyao.installapkdemo.utils.SystemManager;
import java.io.File;
/**
* If there is no bug, then it is created by ChenFengYao on 2017/4/20,
* otherwise, I do not know who create it either.
*/
public class DownloadService extends Service {
private DownloadManager mDownloadManager;
private DownloadBinder mBinder = new DownloadBinder();
private LongSparseArray mApkPaths;
private boolean mIsRoot = false;
private DownloadFinishReceiver mReceiver;
@Override
public void onCreate() {
super.onCreate();
mDownloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
mApkPaths = new LongSparseArray<>();
//注册下载完成的广播
mReceiver = new DownloadFinishReceiver();
registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
unregisterReceiver(mReceiver);//取消注册广播接收者
super.onDestroy();
}
public class DownloadBinder extends Binder{
/**
* 下载
* @param apkUrl 下载的url
*/
public long startDownload(String apkUrl){
//点击下载
//删除原有的APK
IOUtils.clearApk(DownloadService.this,"test.apk");
//使用DownLoadManager来下载
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//将文件下载到自己的Download文件夹下,必须是External的
//这是DownloadManager的限制
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
request.setDestinationUri(Uri.fromFile(file));
//添加请求 开始下载
long downloadId = mDownloadManager.enqueue(request);
Log.d("DownloadBinder", file.getAbsolutePath());
mApkPaths.put(downloadId,file.getAbsolutePath());
return downloadId;
}
public void setInstallMode(boolean isRoot){
mIsRoot = isRoot;
}
/**
* 获取进度信息
* @param downloadId 要获取下载的id
* @return 进度信息 max-100
*/
public int getProgress(long downloadId) {
//查询进度
DownloadManager.Query query = new DownloadManager.Query()
.setFilterById(downloadId);
Cursor cursor = null;
int progress = 0;
try {
cursor = mDownloadManager.query(query);//获得游标
if (cursor != null && cursor.moveToFirst()) {
//当前的下载量
int downloadSoFar = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
//文件总大小
int totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
progress = (int) (downloadSoFar * 1.0f / totalBytes * 100);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return progress;
}
}
//下载完成的广播
private class DownloadFinishReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
//下载完成的广播接收者
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
String apkPath = mApkPaths.get(completeDownloadId);
Log.d("DownloadFinishReceiver", apkPath);
if (!apkPath.isEmpty()){
SystemManager.setPermission(apkPath);//提升读写权限,否则可能出现解析异常
InstallUtil.install(context,apkPath,mIsRoot);
}else {
Log.e("DownloadFinishReceiver", "apkPath is null");
}
}
}
}
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<ProgressBar
android:id="@+id/down_progress"
android:max="100"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/down_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始下载"/>
<Switch
android:id="@+id/install_mode_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="普通模式"
/>
LinearLayout>
布局文件就比较简单了,progressBar来显示进度,switch来切换模式,然后就是一个下载的按钮
package com.example.chenfengyao.installapkdemo;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Switch;
import android.widget.Toast;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class MainActivity extends AppCompatActivity {
private static final String APK_URL = "http://101.28.249.94/apk.r1.market.hiapk.com/data/upload/apkres/2017/4_11/15/com.baidu.searchbox_034250.apk";
private Switch installModeSwitch;
private ProgressBar mProgressBar;
private Button mDownBtn;
private DownloadService.DownloadBinder mDownloadBinder;
private Disposable mDisposable;//可以取消观察者
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mDownloadBinder = (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
mDownloadBinder = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
installModeSwitch = (Switch) findViewById(R.id.install_mode_switch);
mProgressBar = (ProgressBar) findViewById(R.id.down_progress);
mDownBtn = (Button) findViewById(R.id.down_btn);
Intent intent = new Intent(this, DownloadService.class);
startService(intent);
bindService(intent, mConnection, BIND_AUTO_CREATE);//绑定服务
installModeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
buttonView.setText("root模式");
} else {
buttonView.setText("普通模式");
}
if (mDownloadBinder != null) {
mDownloadBinder.setInstallMode(isChecked);
}
});
mDownBtn.setOnClickListener(v -> {
if (mDownloadBinder != null) {
long downloadId = mDownloadBinder.startDownload(APK_URL);
startCheckProgress(downloadId);
}
});
}
@Override
protected void onDestroy() {
if (mDisposable != null) {
//取消监听
mDisposable.dispose();
}
super.onDestroy();
}
//开始监听进度
private void startCheckProgress(long downloadId) {
Observable
.interval(100, 200, TimeUnit.MILLISECONDS, Schedulers.io())//无限轮询,准备查询进度,在io线程执行
.filter(times -> mDownloadBinder != null)
.map(i -> mDownloadBinder.getProgress(downloadId))//获得下载进度
.takeUntil(progress -> progress >= 100)//返回true就停止了,当进度>=100就是下载完成了
.distinct()//去重复
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new ProgressObserver());
}
//观察者
private class ProgressObserver implements Observer<Integer> {
@Override
public void onSubscribe(Disposable d) {
mDisposable = d;
}
@Override
public void onNext(Integer progress) {
mProgressBar.setProgress(progress);//设置进度
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
Toast.makeText(MainActivity.this, "出错", Toast.LENGTH_SHORT).show();
}
@Override
public void onComplete() {
mProgressBar.setProgress(100);
Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show();
}
}
}
下载地址 : http://download.csdn.net/download/cfy137000/9820195