android 7.0 安裝apk

1. 整體需求

  1. 下載APK文件 
    • 使用DownloadManager來下載
    • 在應用界面中展示下載進度
  2. 安裝下載後的APK文件 
    • root模式: 可以自動安裝,不需要用戶主動點擊
    • 正常模式: 彈出安裝應用頁面,需要兼容7.0以上版本

2. DownloadManager

DownloadManager是Android提供的用於下載的類,使用起來比較簡單,它包含兩個靜態內部類DownloadManager.Query和DownloadManager.Request; 
DownloadManager.Request用來請求一個下載,DownloadManager.Query用來查詢下載信息

2.1. 下載

1. 獲取DownloadManager對象

DownloadManager對象屬於系統服務,通過getSystemService來進行安裝

DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
  • 1

一般獲取完成後會變成全局變量,方便之後使用

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

首先會創建出一個DownloadManager.Request對象,在構造方法中接收Uri,其實就是下載地址, 
然後是文件的存放路徑,這裏需要說明,DownloadManager下載的位置是不能放到內置存貯位置的,必須放到Enviroment中,這裏建議放到自己應用的文件夾,不要直接放到SD卡中,也就是通過getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)獲取到的路徑,該位置的文件是屬於應用自己的,在應用卸載時也會隨着應用一起被刪除掉,並且在使用該文件夾的時候,是不需要SD卡讀寫權限的 
然後通過request.setDestinationUri來設置存儲位置,最後將請求加入到downloadManager中,會獲得一個downloadID,這個downloadID比較重要,之後下載狀態,進度的查詢都靠這個downloadID

2.2. 進度查詢

在查詢下載進度的時候,會通過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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在查詢進度的時候會使用到DownloadManager.Query這個類,在查詢的時候,也是使用的Cursor,跟查詢數據庫是一樣的,進度信息會需要拿到文件的總大小,和當前大小,自己算一下,最後Cursor對象在使用過後不要忘記關閉了

2.3 下載完成

下載完成後,DownloadManager會發送一個廣播,並且會包含downloadId的信息

//下載完成的廣播
private class DownloadFinishReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        //下載完成的廣播接收者
        long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

註冊這個廣播接收者

//註冊下載完成的廣播
mReceiver = new DownloadFinishReceiver();
registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
  • 1
  • 2
  • 3

其他

這裏需要注意一點,在下載完成後需要提升一下文件的讀寫權限,否則在安裝的時候會出現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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

chmod 是Linux下設置文件權限的命令,後面的三個數字每一個代表不同的用戶組 
權限分爲三種:讀(r=4),寫(w=2),執行(x=1) 
那麼這三種權限就可以組成7種不同的權限,分別用1-7這幾個數字代表,例如7 = 4 + 2 + 1,那麼就代表該組用戶擁有可讀,可寫,可執行的權限;5 = 4 + 1,就代表可讀可執行權限 
而三位數字就帶包,該登陸用戶,它所在的組,以及其他人

安裝

1. 普通模式

1. 7.0之前

在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);
  • 1
  • 2
  • 3
  • 4
  • 5

2. 7.0之後

在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來解決

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

需要注意一下幾點: 
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>
  • 1
  • 2
  • 3
  • 4
  • 5

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

相較於之前的代碼,會把Uri改成使用FiliProvider創建的Uri,並且添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)來對目標應用臨時授權該Uri所代表的文件,而且getUriForFile中的authority參數需要填寫清單文件中的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環境下啓動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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.root模式

如果應用已經獲取了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) {
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

這個方法就是將命令寫入到手機的shell中,su就代表root權限了,而命令執行成功的話,會返回0的,接下來是安裝命令

String command = "pm install -r " + mApkPath;
  • 1

-r 代表強制安裝,否則如果手機中已有該應用的話就會安裝失敗了,值得注意的是,要想等待命令執行的結果這個過程是很漫長的,所以在使用命令的時候是需要放到主線程中的

3. 整體項目

在寫完整代碼的時候需要把下載的代碼寫到Service中,否則你的downloadid就得通過別的方式去存儲了,而查詢下載進度,也是需要一直去查了,那麼就需要寫一個循環,並且放到子線程中,我們用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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

這裏面主要用到了刪除之前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環境下啓動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);
                    }
                });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

該類只負責安裝APK,如果是Root模式的話,會首先進行嘗試,如果失敗了,還會調用一次普通模式,進行安裝的,注意root模式安裝的代碼,不要忘記放到子線程中去執行了

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 {

    /**
     * 應用程序運行命令獲取 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();
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

該類主要就是放一些需要寫入到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);//獲得遊標
                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");
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • Service和Client通信是使用Binder來做的,提供開始下載,設置安裝模式和獲取進度的方法
  • DownloadFinishReceiver是用來監聽下載完成的廣播接收者,當下載完成後就直接調用InstallUtil來去自動安裝,廣播再使用過後不要忘記取消監聽了
  • 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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

佈局文件就比較簡單了,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);//綁定服務


        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();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 在Activity中需要startService和bindService都使用,因爲我們需要和Service建立聯繫,又需要讓Service脫離Activity的運行
  • 主要說一下checkProgress的代碼,在該方法中會使用RxJava來達到輪詢的功能 
    • interval: 該操作符會一直無限的發射事件,從1,2,3,一直這樣下去,100代表第一個事件延遲100ms,200代表每個事件之間有200ms的間隔
    • filter: 會過濾掉不符合條件的事件,例如如果binder爲空的話,事件就不往下傳遞了
    • map: 當事件到這裏的時候,就通過binder來查詢一下進度
    • takeUntil: 事件持續到什麼時候爲止,因爲interval是無限發射的,總需要一個結束的情況,就使用這個takeUntil,一直到進度達到100的時候就不再查詢了,相當於跳出循環的條件,會觸發觀察者的onComplete方法
    • distinct: 去重,因爲下載進度會查到很多的重複數據,這些數據沒必要都設置到progressBar中,可以利用該操作符去去重
    • 線程切換: 子線程發佈事件,主線程觀察,要刷新UI嘛
    • 最後訂閱一個觀察者,這個觀察者也是自己的類實現了Observer的接口
  • ProgressObserver: 
    • 在RxJava2中,Observer會在訂閱的時候傳入一個Disposable,該對象可以允許觀察者主動的去取消事件,在Activity的onDestroy中會去取消事件
    • onNext中是設置給ProgressBar進度信息
    • onComplete是下載完成

完整代碼

下載地址 : http://download.csdn.net/download/cfy137000/9820195

發佈了7 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章