下載安裝APK(兼容Android7.0)

我們使用手機(jī)的時(shí)候經(jīng)常會(huì)看到應(yīng)用程序提示升級(jí),大部分應(yīng)用內(nèi)部都需要實(shí)現(xiàn)升級(jí)提醒和應(yīng)用程序文件(APK文件)下載昔脯。

一般寫法都差不多亏狰,比如在啟動(dòng)app的時(shí)候,通過api接口獲得服務(wù)器最新的版本號(hào)蟀瞧,然后和本地的版本號(hào)比較,來判斷是否需要彈出提示框下載条摸,當(dāng)然也可以通過推送的自定義消息來實(shí)現(xiàn)悦污。

我們這里主要討論的是應(yīng)用程序下載,并在通知欄提醒下載完成钉蒲。
實(shí)現(xiàn)過程大致分為三步:

  1. 創(chuàng)建一個(gè)service
  2. 在service啟動(dòng)的時(shí)候創(chuàng)建一個(gè)廣播接受者,用于接受下載完成的廣播
  3. 當(dāng)BroadcastReceiver接受到下載完成的廣播時(shí)切端,開始執(zhí)行安裝。

主要通過系統(tǒng)提供的DownloadManager進(jìn)行下載顷啼,DownloadManager下載完成會(huì)發(fā)送廣播踏枣,具體使用看下面完整的代碼昌屉。如果詳細(xì)了解可以參考Android系統(tǒng)下載管理DownloadManager功能介紹及使用示例下面創(chuàng)建新的文件DownloadService.java

public class DownLoadService extends Service {
    /**廣播接受者*/
    private BroadcastReceiver receiver;
    /**系統(tǒng)下載管理器*/
    private DownloadManager dm;
    /**系統(tǒng)下載器分配的唯一下載任務(wù)id,可以通過這個(gè)id查詢或者處理下載任務(wù)*/
    private long enqueue;
    /**TODO下載地址 需要自己修改,這里隨便找了一個(gè)*/
    private String downloadUrl="http://dakaapp.troila.com/download/daka.apk?v=3.0";
            
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
            
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
            
        receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                install(context);
                //銷毀當(dāng)前的Service
                stopSelf();
            }
        };
        registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        //下載需要寫SD卡權(quán)限, targetSdkVersion>=23 需要?jiǎng)討B(tài)申請(qǐng)權(quán)限
        RxPermissions.getInstance(this)
                // 申請(qǐng)權(quán)限
                .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .subscribe(new Action1<Boolean>() {
                    @Override
                    public void call(Boolean granted) {
                        if(granted){
                            //請(qǐng)求成功
                            startDownload(downloadUrl);
                        }else{
                            // 請(qǐng)求失敗回收當(dāng)前服務(wù)
                            stopSelf();
      
                        }
                    }
                });
        return Service.START_STICKY;
    }
            
    /**
     * 通過隱式意圖調(diào)用系統(tǒng)安裝程序安裝APK
     */
    public static void install(Context context) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于沒有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(Uri.fromFile(
                new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "myApp.apk")),
                "application/vnd.android.package-archive");
        context.startActivity(intent);
    }
            
    @Override
    public void onDestroy() {
        //服務(wù)銷毀的時(shí)候 反注冊(cè)廣播
        unregisterReceiver(receiver);
        super.onDestroy();
    }
            
    private void startDownload(String downUrl) {
        //獲得系統(tǒng)下載器
        dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        //設(shè)置下載地址
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downUrl));
        //設(shè)置下載文件的類型
        request.setMimeType("application/vnd.android.package-archive");
       //設(shè)置下載存放的文件夾和文件名字
 request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "myApp.apk");
        //設(shè)置下載時(shí)或者下載完成時(shí)茵瀑,通知欄是否顯示
 request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setTitle("下載新版本");
        //執(zhí)行下載间驮,并返回任務(wù)唯一id
        enqueue = dm.enqueue(request);
    }
}

上面代碼使用了RxPermissions第三方庫動(dòng)態(tài)申請(qǐng)權(quán)限,需要在app/build.gradle文件中進(jìn)行配置

dependencies {
    //...
    compile 'com.tbruyelle.rxpermissions:rxpermissions:0.7.0@aar'
    compile 'io.reactivex:rxjava:1.1.6' //需要引入RxJava
}

記得要配置服務(wù)

<application
  ...>
    ...
    <service android:name=".DownLoadService"/>
</application>

最后在MainActivity中添加按鈕瘾婿,執(zhí)行操作蜻牢。運(yùn)行結(jié)果:


當(dāng)下載的時(shí)候,會(huì)有通知欄進(jìn)度條提示偏陪。下載完成會(huì)提示安裝抢呆。不過當(dāng)前程序如果在Android7.0上就會(huì)報(bào)錯(cuò)。下面是報(bào)錯(cuò)的日志:


Caused by: android.os.FileUriExposedException: 
file:///storage/emulated/0/Download/myApp.apk exposed beyond app through Intent.getData()

這是由于Android7.0執(zhí)行了“StrictMode API 政策禁”的原因笛谦,不過小伙伴們不用擔(dān)心抱虐,可以用FileProvider來解決這一問題,

現(xiàn)在我們就來一步一步的解決這個(gè)問題饥脑。

Android 7.0錯(cuò)誤原因

隨著Android版本越來越高恳邀,Android對(duì)隱私的保護(hù)力度也越來越大。

比如:Android6.0引入的動(dòng)態(tài)權(quán)限控制(Runtime Permissions)灶轰,Android7.0又引入“私有目錄被限制訪問”谣沸,“StrictMode API 政策”。

這些更改在為用戶帶來更加安全的操作系統(tǒng)的同時(shí)也為開發(fā)者帶來了一些新的任務(wù)笋颤。如何讓你的APP能夠適應(yīng)這些改變而不是crash乳附,是擺在每一位Android開發(fā)者身上的責(zé)任。

“私有目錄被限制訪問“ 是指在Android7.0中為了提高私有文件的安全性伴澄,面向 Android N 或更高版本的應(yīng)用私有目錄將被限制訪問赋除。這點(diǎn)類似iOS的沙盒機(jī)制。

" StrictMode API 政策" 是指禁止向你的應(yīng)用外公開 file:// URI非凌。 如果一項(xiàng)包含文件 file:// URI類型 的 Intent 離開你的應(yīng)用举农,應(yīng)用失敗,并出現(xiàn) FileUriExposedException 異常敞嗡。

上面用到的代碼中的Uri.fromFile 其實(shí)就是生成一個(gè)file://URL颁糟。

//...
intent.setDataAndType(Uri.fromFile(
                new File(Environment.getExternalStoragePublicDirectory(
                  Environment.DIRECTORY_DOWNLOADS), 
                         "myApp.apk")),
                "application/vnd.android.package-archive");
       
//....

一旦我們通過這種辦法打開其它程序(這里打開系統(tǒng)包安裝器)就認(rèn)為file:// URI類型的 Intent 離開你的應(yīng)用。這樣程序就會(huì)發(fā)生異常喉悴。

接下來就用FileProvider來解決這一問題棱貌。

使用FileProvider

使用FileProvider的大致步驟如下:

第一步:
在AndroidManifest.xml清單文件中注冊(cè)provider,因?yàn)閜rovider也是Android四大組件之一粥惧,可以簡(jiǎn)單把它理解為向外提供數(shù)據(jù)的組件键畴,這種組件在實(shí)際開發(fā)中用的頻率并不高最盅,四大組件都可以在清單文件中進(jìn)行配置突雪。

<application
   ...>
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.yll520wcf.test.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <!--元數(shù)據(jù)-->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

注意:

  • exported:要求必須為false起惕,為true則會(huì)報(bào)安全異常。
  • grantUriPermissions:true咏删,表示授予 URI 臨時(shí)訪問權(quán)限惹想。
  • authorities 組件標(biāo)識(shí),按照江湖規(guī)矩,都以包名開頭,避免和其它應(yīng)用發(fā)生沖突督函。

第二步:指定共享的目錄
上面配置文件中 android:resource="@xml/file_paths" 指的是當(dāng)前組件引用 res/xml/file_paths.xml 這個(gè)文件嘀粱。

我們需要在資源(res)目錄下創(chuàng)建一個(gè)xml目錄,然后創(chuàng)建一個(gè)名為“file_paths”(名字可以隨便起辰狡,只要和在manifest注冊(cè)的provider所引用的resource保持一致即可)的資源文件锋叨,內(nèi)容如下:

  • <files-path/>代表的根目錄: Context.getFilesDir()
  • <external-path/>代表的根目錄: Environment.getExternalStorageDirectory()
  • <cache-path/>代表的根目錄: getCacheDir()

上述代碼中path="",是有特殊意義的宛篇,它代碼根目錄娃磺,也就是說你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個(gè)文件了。

如果你將path設(shè)為path="pictures"叫倍,那么它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures)偷卧,如果你向其它應(yīng)用分享pictures目錄范圍之外的文件是不行的。

第三步:使用FileProvider
上述準(zhǔn)備工作做完之后吆倦,現(xiàn)在我們就可以使用FileProvider了听诸。
我們需要將上述安裝APK代碼修改為如下

public static void install(Context context) {
    File file= new File(
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
            , "myApp.apk");
    //參數(shù)1 上下文, 參數(shù)2 Provider主機(jī)地址 和配置文件中保持一致   參數(shù)3  共享的文件
    Uri apkUri =
            FileProvider.getUriForFile(context, "com.com.yll520wcf.test.fileprovider", file);
    
    Intent intent = new Intent(Intent.ACTION_VIEW);
    // 由于沒有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //添加這一句表示對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    context.startActivity(intent);
}

上述代碼中主要有兩處改變:

  1. 將之前Uri改成了有FileProvider創(chuàng)建一個(gè)content類型的Uri。
  2. 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);來對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該Uri所代表的文件蚕泽。

上述代碼通過FileProviderUri getUriForFile (Context context, String authority, File file)靜態(tài)方法來獲取Uri
該方法中authority參數(shù)就是清單文件中注冊(cè)provider時(shí)填寫的authority

android:authorities="com.yll520wcf.test.fileprovider"

按照上面步驟修改就可以兼容Android7.0了晌梨。

后期修改,之前沒有考慮7.0以下的版本

但是如果此程序在Android7.0以下運(yùn)行又會(huì)報(bào)錯(cuò)了赛糟,我們需要通過版本判斷派任,當(dāng)Android7.0及以上需要調(diào)用上面的代碼,Android7.0以下需要調(diào)用7.0以下的代碼璧南。這樣就OK了掌逛。修改install() 方法代碼。

    /**
     * 通過隱式意圖調(diào)用系統(tǒng)安裝程序安裝APK
     */
    public static void install(Context context) {
        File file = new File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                , "myApp.apk");
        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于沒有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if(Build.VERSION.SDK_INT>=24) { //判讀版本是否在7.0以上
            //參數(shù)1 上下文, 參數(shù)2 Provider主機(jī)地址 和配置文件中保持一致   參數(shù)3  共享的文件
            Uri apkUri =
                    FileProvider.getUriForFile(context, "com.a520wcf.chapter11.fileprovider", file);
            //添加這一句表示對(duì)目標(biāo)應(yīng)用臨時(shí)授權(quán)該Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        }else{
            intent.setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
        }
        context.startActivity(intent);
    }

參考文獻(xiàn)

android應(yīng)用開發(fā)app手動(dòng)更新通知欄下載實(shí)踐
Android7.0適配教程司倚,心得

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末豆混,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子动知,更是在濱河造成了極大的恐慌皿伺,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盒粮,死亡現(xiàn)場(chǎng)離奇詭異鸵鸥,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門妒穴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宋税,“玉大人,你說我怎么就攤上這事讼油〗苋” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵矮台,是天一觀的道長(zhǎng)乏屯。 經(jīng)常有香客問我,道長(zhǎng)瘦赫,這世上最難降的妖魔是什么辰晕? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮确虱,結(jié)果婚禮上伞芹,老公的妹妹穿的比我還像新娘。我一直安慰自己蝉娜,他們只是感情好唱较,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著召川,像睡著了一般南缓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荧呐,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天汉形,我揣著相機(jī)與錄音,去河邊找鬼倍阐。 笑死概疆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的峰搪。 我是一名探鬼主播岔冀,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼概耻!你這毒婦竟也來了使套?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤鞠柄,失蹤者是張志新(化名)和其女友劉穎侦高,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厌杜,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奉呛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞧壮。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡危尿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出馁痴,到底是詐尸還是另有隱情,我是刑警寧澤肺孤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布罗晕,位于F島的核電站,受9級(jí)特大地震影響赠堵,放射性物質(zhì)發(fā)生泄漏小渊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一茫叭、第九天 我趴在偏房一處隱蔽的房頂上張望酬屉。 院中可真熱鬧,春花似錦揍愁、人聲如沸呐萨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谬擦。三九已至,卻和暖如春朽缎,著一層夾襖步出監(jiān)牢的瞬間惨远,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工话肖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留北秽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓最筒,卻偏偏與公主長(zhǎng)得像贺氓,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子床蜘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

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

  • 轉(zhuǎn)載自于連林 我們使用手機(jī)的時(shí)候經(jīng)常會(huì)看到應(yīng)用程序提示升級(jí)掠归,大部分應(yīng)用內(nèi)部都需要實(shí)現(xiàn)升級(jí)提醒和應(yīng)用程序文件(APK...
    sirai閱讀 1,110評(píng)論 0 24
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,510評(píng)論 25 707
  • Android7.0發(fā)布已經(jīng)有一個(gè)多月了,Android7.0在給用戶帶來一些新的特性的同時(shí)悄泥,也給開發(fā)者帶來了新的...
    東經(jīng)315度閱讀 1,343評(píng)論 0 14
  • Android7.0發(fā)布已經(jīng)有一個(gè)多月了弹囚,Android7.0在給用戶帶來一些新的特性的同時(shí)厨相,也給開發(fā)者帶來了新的...
    CrazyCodeBoy閱讀 77,248評(píng)論 46 745
  • 2017年已經(jīng)過去了大半年,還有兩個(gè)月就要迎接新的一年了践磅,那么在2017年的這十個(gè)月里单刁,有多少華語片是值得一看的呢...
    _北鬼閱讀 2,036評(píng)論 0 2