Android7.0以上版本更新,報"解析包異常"

前言:一個人至少擁有一個夢想,有一個理由去堅強。心若沒有棲息的地方,到哪里都是在流浪 ---三毛

現(xiàn)在市面上的app基本使用了更新功能,而且在android7.0以上的手機沒有做處理,故記錄之

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

這里主要討論的是應(yīng)用程序下載,并在通知欄提醒下載完成。 實現(xiàn)過程大致分為三步:
1.創(chuàng)建一個service
2.在service啟動的時候創(chuàng)建一個廣播接受者,用于接受下載完成的廣播
3.當(dāng)BroadcastReceiver接受到下載完成的廣播時,開始執(zhí)行安裝。

主要通過系統(tǒng)提供的DownloadManager進行下載,DownloadManager下載完成會發(fā)送廣播,具體使用看下面完整的代碼。如果詳細了解可參考[android系統(tǒng)下載管理DownloadManager功能介紹及使用實例] (http://www.trinea.cn/android/android-downloadmanager)

public class DownLoadService extends Service {
    /**廣播接受者*/
    private BroadcastReceiver receiver;
    /**系統(tǒng)下載管理器*/
    private DownloadManager dm;
    /**系統(tǒng)下載器分配的唯一下載任務(wù)id,可以通過這個id查詢或者處理下載任務(wù)*/
    private long enqueue;
    /**TODO下載地址 需要自己修改,這里隨便找了一個*/
    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 需要動態(tài)申請權(quán)限
        RxPermissions.getInstance(this)
                // 申請權(quán)限
                .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .subscribe(new Action1<Boolean>() {
                    @Override
                    public void call(Boolean granted) {
                        if(granted){
                            //請求成功
                            startDownload(downloadUrl);
                        }else{
                            // 請求失敗回收當(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)境下啟動Activity,設(shè)置下面的標簽
        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ù)銷毀的時候 反注冊廣播
        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è)置下載時或者下載完成時,通知欄是否顯示
        request.setNotificationVisibility(DownloadManager.
        Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setTitle("下載新版本");
        //執(zhí)行下載,并返回任務(wù)唯一id
        enqueue = dm.enqueue(request);
    }
}

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

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>
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)在我們就來一步一步的解決這個問題。

Android 7.0以上版本更新錯誤原因

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

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

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

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

上面用到的代碼中的Uri.fromFile 其實就是生成一個file://URL。

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

一旦我們通過這種辦法打開其它程序(這里打開系統(tǒng)包安裝器)就認為file:// URI類型的 Intent 離開你的應(yīng)用。這樣程序就會發(fā)生異常。
接下來就用 FileProvider 來解決這一問題。

使用 FileProvider:

使用FileProvider的大致步驟如下:

第一步:

在AndroidManifest.xml清單文件中注冊provider,因為provider也是Android四大組件之一,可以簡單把它理解為向外提供數(shù)據(jù)的組件,這種組件在實際開發(fā)中用的頻率并不高,四大組件都可以在清單文件中進行配置。

<application
   ...>
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.tyj.tourapp.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則會報安全異常。
?grantUriPermissions:true,表示授予 URI 臨時訪問權(quán)
限。
?authorities 組件標識,按照江湖規(guī)矩,都以包名開頭,避免和其它應(yīng)用發(fā)生沖突。

第二步:指定共享的目錄

上面配置文件中 android:resource="@xml/file_paths" 指的是當(dāng)前組件引用 res/xml/file_paths.xml 這個文件。

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

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path path="Android/data/com.tyj.tourapp.fileprovider/" name="files_root" />
    <external-path path="." name="external_storage_root" />
</paths>

?代表的根目錄: Context.getFilesDir()
?代表的根目錄: Environment.getExternalStorageDirectory()
?代表的根目錄: getCacheDir()

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

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

第三步:使用FileProvider

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

 /**
     * 通過隱式意圖調(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)境下啟動Activity,設(shè)置下面的標簽
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if(Build.VERSION.SDK_INT>=24) { //判讀版本是否在7.0以上
            //參數(shù)1 上下文, 參數(shù)2 Provider主機地址 和配置文件中保持一致   參數(shù)3  共享的文件
            Uri apkUri =
                    FileProvider.getUriForFile(context, "com.a520wcf.chapter11.fileprovider", file);
            //添加這一句表示對目標應(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(file),
                    "application/vnd.android.package-archive");
        }
        context.startActivity(intent);
    }

這樣我們就能愉快的解決問題了,希望能幫助大家

參考文獻

[android應(yīng)用開發(fā)app手動更新通知欄下載實踐]{http://www.itdecent.cn/p/e4f1dc089740}
[Android7.0適配教程心得]{http://www.itdecent.cn/p/56b9fb319310}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,765評論 25 709
  • Android7.0發(fā)布已經(jīng)有一個多月了,Android7.0在給用戶帶來一些新的特性的同時,也給開發(fā)者帶來了新的...
    東經(jīng)315度閱讀 1,414評論 0 14
  • 從Android6.0引入的動態(tài)權(quán)限控制(Runtime Permissions)到Android7.0的“私有目...
    黃海佳閱讀 1,570評論 0 1
  • 關(guān)于減肥的14條黃金法則,減肥君的日常生活,你該這么做: 1、早餐,喝牛奶,記住是脫脂牛奶,不要加奶油或者糖的咖啡...
    高大勝閱讀 478評論 0 2
  • 他又出差了,臨走前親親女兒,抱抱我。這種分別的感覺很真切,像小時候爸爸每次去外地干活都會留個字條給我。爸爸寫字很好...
    如水如天閱讀 144評論 0 0

友情鏈接更多精彩內(nèi)容