Android7.0下載Apk自動安裝
1. 整體需求
- 下載APK文件
- 使用DownloadManager來下載
- 在應(yīng)用界面中展示下載進度
- 安裝下載后的APK文件
- root模式: 可以自動安裝,不需要用戶主動點擊
- 正常模式: 彈出安裝應(yīng)用頁面,需要兼容7.0以上版本
2. DownloadManager
DownloadManager是Android提供的用于下載的類,使用起來比較簡單,它包含兩個靜態(tài)內(nèi)部類DownloadManager.Query和DownloadManager.Request;
DownloadManager.Request用來請求一個下載,DownloadManager.Query用來查詢下載信息
2.1. 下載
1. 獲取DownloadManager對象
DownloadManager對象屬于系統(tǒng)服務(wù),通過getSystemService來進行安裝
DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
一般獲取完成后會變成全局變量,方便之后使用
2. 開始下載
在使用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);
首先會創(chuàng)建出一個DownloadManager.Request對象,在構(gòu)造方法中接收Uri,其實就是下載地址,
然后是文件的存放路徑,這里需要說明,DownloadManager下載的位置是不能放到內(nèi)置存貯位置的,必須放到Enviroment中,這里建議放到自己應(yīng)用的文件夾,不要直接放到SD卡中,也就是通過getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)獲取到的路徑,該位置的文件是屬于應(yīng)用自己的,在應(yīng)用卸載時也會隨著應(yīng)用一起被刪除掉,并且在使用該文件夾的時候,是不需要SD卡讀寫權(quán)限的
然后通過request.setDestinationUri來設(shè)置存儲位置,最后將請求加入到downloadManager中,會獲得一個downloadID,這個downloadID比較重要,之后下載狀態(tài),進度的查詢都靠這個downloadID
2.2. 進度查詢
在查詢下載進度的時候,會通過downloadId來指定查詢某一任務(wù)的具體進度
/**
* 獲取進度信息
* @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);//獲得游標(biāo)
if (cursor != null && cursor.moveToFirst()) {
//當(dāng)前的下載量
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,跟查詢數(shù)據(jù)庫是一樣的,進度信息會需要拿到文件的總大小,和當(dāng)前大小,自己算一下,最后Cursor對象在使用過后不要忘記關(guān)閉了
2.3 下載完成
下載完成后,DownloadManager會發(fā)送一個廣播,并且會包含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));
其他
這里需要注意一點,在下載完成后需要提升一下文件的讀寫權(quán)限,否則在安裝的時候會出現(xiàn)apk解析失敗的頁面,就是別人訪問不了我們的apk文件
/**
* 提升讀寫權(quán)限
* @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下設(shè)置文件權(quán)限的命令,后面的三個數(shù)字每一個代表不同的用戶組
權(quán)限分為三種:讀(r=4),寫(w=2),執(zhí)行(x=1)
那么這三種權(quán)限就可以組成7種不同的權(quán)限,分別用1-7這幾個數(shù)字代表,例如7 = 4 + 2 + 1,那么就代表該組用戶擁有可讀,可寫,可執(zhí)行的權(quán)限;5 = 4 + 1,就代表可讀可執(zhí)行權(quán)限
而三位數(shù)字就帶包,該登陸用戶,它所在的組,以及其他人
安裝
1. 普通模式
1. 7.0之前
在7.0之前安裝的時候,只需要通過隱式Intent來跳轉(zhuǎn),并且指定安裝的文件Uri即可
Intent intent = new Intent(Intent.ACTION_VIEW);
// 由于沒有在Activity環(huán)境下啟動Activity,設(shè)置下面的標(biāo)簽
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");context.startActivity(intent);
2. 7.0之后
在Android7.0之后的版本運行上述代碼會出現(xiàn) android.os.FileUriExposedException
"私有目錄被限制訪問"是指在Android7.0中為了提高私有文件的安全性塘安,面向 Android N 或更高版本的應(yīng)用私有目錄將被限制訪問斯嚎。
而7.0的" StrictMode API 政策" 是指禁止向你的應(yīng)用外公開 file:// URI壁榕。 如果一項包含文件 file:// URI類型 的 Intent 離開你的應(yīng)用山孔,應(yīng)用失敗,并出現(xiàn) FileUriExposedException 異常宴卖。
之前代碼用到的Uri.fromFile就是商城一個file://的Uri
在7.0之后,我們需要使用FileProvider來解決
FileProvider
第一步:
在AndroidManifest.xml清單文件中注冊provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.chenfengyao.installapkdemo"
android:grantUriPermissions="true"
android:exported="false">
<!--元數(shù)據(jù)-->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
需要注意一下幾點:
- exported:必須為false
- grantUriPermissions:true纺阔,表示授予 URI 臨時訪問權(quán)限。
- authorities 組件標(biāo)識悼瓮,都以包名開頭,避免和其它應(yīng)用發(fā)生沖突。
第二步:
指定共享文件的目錄,需要在res文件夾中新建xml目錄,并且創(chuàng)建file_paths
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<external-path path="" name="download"/>
</paths>
</resources>
path=""艰猬,是有特殊意義的横堡,它代表根目錄,也就是說你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個文件了冠桃。
第三部:
使用FileProvider
Intent intent = new Intent(Intent.ACTION_VIEW);
File file = (new File(apkPath));
// 由于沒有在Activity環(huán)境下啟動Activity,設(shè)置下面的標(biāo)簽
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//參數(shù)1 上下文, 參數(shù)2 Provider主機地址 和配置文件中保持一致 參數(shù)3 共享的文件
Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
//添加這一句表示對目標(biāo)應(yīng)用臨時授權(quán)該Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
context.startActivity(intent);
相較于之前的代碼,會把Uri改成使用FiliProvider創(chuàng)建的Uri,并且添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)來對目標(biāo)應(yīng)用臨時授權(quán)該Uri所代表的文件,而且getUriForFile中的authority參數(shù)需要填寫清單文件中的authorities的值
3. 混合
兼容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環(huán)境下啟動Activity,設(shè)置下面的標(biāo)簽
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//參數(shù)1 上下文, 參數(shù)2 Provider主機地址 和配置文件中保持一致 參數(shù)3 共享的文件
Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
//添加這一句表示對目標(biāo)應(yīng)用臨時授權(quán)該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);
}
2.root模式
如果應(yīng)用已經(jīng)獲取了root權(quán)限了,那么我們可以實現(xiàn)自動安裝,即不會出現(xiàn)應(yīng)用安裝的頁面,會在后臺自己慢慢的安裝,這個時候使用的就是用代碼去寫命令行了
/**
* 應(yīng)用程序運行命令獲取 Root權(quán)限命贴,設(shè)備必須已破解(獲得ROOT權(quán)限)
*
* @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath(); RootCommand(apkRoot);
* @return 0 命令執(zhí)行成功
*/
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權(quán)限了,而命令執(zhí)行成功的話,會返回0的,接下來是安裝命令
String command = "pm install -r " + mApkPath;
-r 代表強制安裝,否則如果手機中已有該應(yīng)用的話就會安裝失敗了,值得注意的是,要想等待命令執(zhí)行的結(jié)果這個過程是很漫長的,所以在使用命令的時候是需要放到主線程中的
3. 整體項目
在寫完整代碼的時候需要把下載的代碼寫到Service中,否則你的downloadid就得通過別的方式去存儲了,而查詢下載進度,也是需要一直去查了,那么就需要寫一個循環(huán),并且放到子線程中,我們用RxJava做會比較舒服
![](https://ww4.sinaimg.cn/large/006tKfTcly1fet48lffzxg309a0fkh9z.gif)
1. 一些工具代碼
1. IOUtils
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的代碼,下載前如果有歷史版本,就把它刪掉,下載新的
2. InstallUtil
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環(huán)境下啟動Activity,設(shè)置下面的標(biāo)簽
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//參數(shù)1 上下文, 參數(shù)2 Provider主機地址 和配置文件中保持一致 參數(shù)3 共享的文件
Uri apkUri = FileProvider.getUriForFile(context, "com.example.chenfengyao.installapkdemo", file);
//添加這一句表示對目標(biāo)應(yīng)用臨時授權(quán)該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權(quán)限獲取失敗,嘗試普通安裝", Toast.LENGTH_SHORT).show();
install(context,apkPath);
}
});
}
}
該類只負責(zé)安裝APK,如果是Root模式的話,會首先進行嘗試,如果失敗了,還會調(diào)用一次普通模式,進行安裝的,注意root模式安裝的代碼,不要忘記放到子線程中去執(zhí)行了
3. SystemManager
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 {
/**
* 應(yīng)用程序運行命令獲取 Root權(quán)限,設(shè)備必須已破解(獲得ROOT權(quán)限)
*
* @param command 命令:String apkRoot="chmod 777 "+getPackageCodePath();
* @return 0 命令執(zhí)行成功
*/
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) {
}
}
}
/**
* 提升讀寫權(quán)限
* @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中的代碼
2. DownLoadService
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<String> 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);//獲得游標(biāo)
if (cursor != null && cursor.moveToFirst()) {
//當(dāng)前的下載量
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);//提升讀寫權(quán)限,否則可能出現(xiàn)解析異常
InstallUtil.install(context,apkPath,mIsRoot);
}else {
Log.e("DownloadFinishReceiver", "apkPath is null");
}
}
}
}
- Service和Client通信是使用Binder來做的,提供開始下載,設(shè)置安裝模式和獲取進度的方法
- DownloadFinishReceiver是用來監(jiān)聽下載完成的廣播接收者,當(dāng)下載完成后就直接調(diào)用InstallUtil來去自動安裝,廣播再使用過后不要忘記取消監(jiān)聽了
- LongSparseArray 可以理解為key值是long類型的HashMap,但是效率要稍高一點,在Android中都推薦使用各種的SparseArray
3. Activity
1. 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="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>
![](https://ww1.sinaimg.cn/large/006tNbRwly1fet0apcfv5j30nw0jsq3n.jpg)
布局文件就比較簡單了,progressBar來顯示進度,switch來切換模式,然后就是一個下載的按鈕
2. Activity
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);//綁定服務(wù)
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) {
//取消監(jiān)聽
mDisposable.dispose();
}
super.onDestroy();
}
//開始監(jiān)聽進度
private void startCheckProgress(long downloadId) {
Observable
.interval(100, 200, TimeUnit.MILLISECONDS, Schedulers.io())//無限輪詢,準備查詢進度,在io線程執(zhí)行
.filter(times -> mDownloadBinder != null)
.map(i -> mDownloadBinder.getProgress(downloadId))//獲得下載進度
.takeUntil(progress -> progress >= 100)//返回true就停止了,當(dāng)進度>=100就是下載完成了
.distinct()//去重復(fù)
.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);//設(shè)置進度
}
@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();
}
}
}
- 在Activity中需要startService和bindService都使用,因為我們需要和Service建立聯(lián)系,又需要讓Service脫離Activity的運行
- 主要說一下checkProgress的代碼,在該方法中會使用RxJava來達到輪詢的功能
- interval: 該操作符會一直無限的發(fā)射事件,從1,2,3,一直這樣下去,100代表第一個事件延遲100ms,200代表每個事件之間有200ms的間隔
- filter: 會過濾掉不符合條件的事件,例如如果binder為空的話,事件就不往下傳遞了
- map: 當(dāng)事件到這里的時候,就通過binder來查詢一下進度
- takeUntil: 事件持續(xù)到什么時候為止,因為interval是無限發(fā)射的,總需要一個結(jié)束的情況,就使用這個takeUntil,一直到進度達到100的時候就不再查詢了,相當(dāng)于跳出循環(huán)的條件,會觸發(fā)觀察者的onComplete方法
- distinct: 去重,因為下載進度會查到很多的重復(fù)數(shù)據(jù),這些數(shù)據(jù)沒必要都設(shè)置到progressBar中,可以利用該操作符去去重
- 線程切換: 子線程發(fā)布事件,主線程觀察,要刷新UI嘛
- 最后訂閱一個觀察者,這個觀察者也是自己的類實現(xiàn)了Observer的接口
- ProgressObserver:
- 在RxJava2中,Observer會在訂閱的時候傳入一個Disposable,該對象可以允許觀察者主動的去取消事件,在Activity的onDestroy中會去取消事件
- onNext中是設(shè)置給ProgressBar進度信息
- onComplete是下載完成