Android7.0下載Apk自動安裝

Android7.0下載Apk自動安裝

1. 整體需求

  1. 下載APK文件
    • 使用DownloadManager來下載
    • 在應(yīng)用界面中展示下載進度
  2. 安裝下載后的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>

需要注意一下幾點:

  1. exported:必須為false
  2. grantUriPermissions:true纺阔,表示授予 URI 臨時訪問權(quán)限。
  3. 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做會比較舒服


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>

布局文件就比較簡單了,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是下載完成
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末食听,一起剝皮案震驚了整個濱河市胸蛛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌樱报,老刑警劉巖葬项,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異迹蛤,居然都是意外死亡民珍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門笤受,熙熙樓的掌柜王于貴愁眉苦臉地迎上來穷缤,“玉大人敌蜂,你說我怎么就攤上這事箩兽。” “怎么了章喉?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵汗贫,是天一觀的道長。 經(jīng)常有香客問我秸脱,道長落包,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任摊唇,我火速辦了婚禮咐蝇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘巷查。我一直安慰自己有序,他們只是感情好抹腿,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著旭寿,像睡著了一般警绩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盅称,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天肩祥,我揣著相機與錄音,去河邊找鬼缩膝。 笑死混狠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逞盆。 我是一名探鬼主播檀蹋,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼云芦!你這毒婦竟也來了俯逾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤舅逸,失蹤者是張志新(化名)和其女友劉穎桌肴,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琉历,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡坠七,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了旗笔。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片彪置。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蝇恶,靈堂內(nèi)的尸體忽然破棺而出拳魁,到底是詐尸還是另有隱情,我是刑警寧澤撮弧,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布潘懊,位于F島的核電站,受9級特大地震影響贿衍,放射性物質(zhì)發(fā)生泄漏授舟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一贸辈、第九天 我趴在偏房一處隱蔽的房頂上張望释树。 院中可真熱鬧,春花似錦、人聲如沸奢啥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扫尺。三九已至筋栋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間正驻,已是汗流浹背弊攘。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姑曙,地道東北人襟交。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像伤靠,于是被迫代替她去往敵國和親捣域。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,144評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理宴合,服務(wù)發(fā)現(xiàn)焕梅,斷路器,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 從Android 2.3(API level 9)開始Android用系統(tǒng)服務(wù)(Service)的方式提供了Dow...
    柨柨閱讀 2,712評論 1 4
  • 中文故事兩本卦洽,英語小書3本 和萌一起玩賣扣子游戲贞言,萌對金錢還沒有概念呢,以后慢慢輸入 和萌一起補記昨天的抱箱子日記...
    艷萍和萌寶閱讀 193評論 0 0
  • 天冷了阀蒂,開車方向盤也是冷的该窗。一年的時間過的好快,已想到隆冬后是春暖花開的季節(jié)蚤霞。 想到這周的大組會酗失,想到翰明陽,有點...
    天之心語閱讀 273評論 1 1