天天看點

Android 5.0、6.0、7.0、8.0、9.0 新特性,DownloadManager踩坑記前言正文

目錄

一、5.0新特性

二、6.0新特性

三、7.0新特性

四、8.0新特性

五、9.0新特性

六、 一步步跟着案例進行版本更新踩坑——DownloadManager

1、适配6.0 動态權限

2、适配Android 7.0 解析包時出現問題

3、适配Android 8.0:未知來源的應用權限

本案例下載下傳位址:

https://download.csdn.net/download/csdn_aiyang/10906816

https://github.com/aiyangtianci/DownloadManagerDemo

前言

Android 最近幾年發展非常快,自08年智能機時代開始,Android作業系統一路高歌,每年推出更新新的版本。然而,這一轉眼都10年過去了,10年的智能機發展之路,使Android系統占全球市場佔有率近7成(IOS 近3成)。有趣的是谷歌給自己的這個‘親兒子’起名字可謂絞盡了腦汁,Android1.1開始,就有過給起名Astro(小金剛)、Bender(機器人班亭)本是想從字母A到字母Z,每一代都用首字母排序,可是,由這樣的名字太二了,于是從Android1.5的時候,聰明的谷歌就想到了以甜點給‘親兒子’命名的文化傳統了,就在17年8月份谷歌為提高續航和安全釋出了Android 8.0,主題是“奧利奧餅幹”。但是,由于版本系統推出過快及相容過多舊版代碼,Android 版本碎片化越來越嚴重了。

谷歌從8.0後推出的一種新的技術架構“Project Treble”,可以改善安卓系統的碎片化問題。将作業系統架構代碼與“特定供應商”硬體代碼分開,允許手機廠商在無須晶片商參與的情況下推送更新,希望由此減少更新阻礙,加快新系統普及速度。在Android 9.0釋出後,谷歌加入了強制性的rollback protection功能。簡單來說,這項功能直接杜絕了使用者的系統降級操作,雖然早在Android 8.0中就已經加入了這項功能,但是不同的是,原本這項功能是處于使用者自選的狀态,在更新Android 9之後就變更為強制性了。這也就寓意着,從Android 9開始,之後的安卓系統終于要正式緻敬蘋果的iOS,系統隻提供更新,不允許降級。

其實,谷歌一直以來都在想盡辦法試圖解決安卓系統碎片化的問題,但是由于覆寫廣、适配裝置過多等因素,始終沒有收獲什麼成效。碎片化這一問題,也始終被使用者所诟病,不過谷歌這次真的下狠手了,通過在系統内置Android Verified Boot 2.0,來阻止使用者降級,一旦使用者強制輸入低于目前版本的安卓系統,被自動檢測到後會出現無限重新開機、無法開機,不得不佩服谷歌的這招真的是做絕了。

正文

在正式開篇之前,本人有些心路曆程想和大家分享一下,說白了,就是想吐吐這幾天苦水,哈哈哈。從收集文章素材、參考文章、動手寫demo、寫文章等,用了近一個星期。我個人覺得自己寫文章的效率很高,這篇文章中的案例在踩坑時出現過很多次奇怪的閃退,着實要費了些心思。例如,在一個手機上開發時遇見崩潰,換個手機就沒問題了,我實在搞不懂崩潰原因時候,也會像小白一樣重新建立新項目,把舊代碼一行行重新複制粘貼過去,重新運作項目去解決問題。然而,就這樣我盡然重新建立過四次。手動捂臉哭表情。說這些,是希望同學們能認真對待我的博文,謝謝,因為我很用心在分享技術!

一、5.0新特性

  • Material Design
  • 支援多種裝置
  • 全新通知中心
  • 支援 64 位 ART 虛拟機
  • 電池續航改進
  • 全新“最近應用程式”
  • 安全性改進
  • 不同資料獨立儲存
  • 改進搜尋
  • 支援藍牙 4.1、USB Audio、多人分享等

重點注意: 加了很多新控件,如抽屜布局,菜單布局,卡片布局,清單布局新增RecyclerView等。努力改善應用界面吧!

Android Material Design之CoordinatorLayout效果實作

Android ToolBar 基礎使用——進階封裝BaseActivity(附源碼)

Android 探究onCreateViewHolder和onBindViewHolder兩者關系和調用次數

Android 使用RecycleView實作吸附小标題的Demo(附源碼)

Android5.0Activity的轉場動畫、過渡動畫、過場動畫、跳轉動畫

Android共享元素轉場動畫Fragment to Fragment

二、6.0新特性

  • 動态權限管理
  • 系統層支援指紋識别
  • APP 關聯
  • Android Pay
  • 電源管理
  • TF 卡預設存儲
重點注意:動态的權限申請,6.0以下的版本可以直接申請權限直接使用了,以上的版本需要一些敏感權限時需要動态申請。可參考我的文章。

Android6.0版本以上危險權限動态申請及RxPermissions權限庫使用

三、7.0新特性

  • 分屏多任務
  • 下拉快捷開關
  • 新通知消息
  • 夜間模式
  • 流量保護模式
  • 全新設定樣式
  • 改進 Doze 休眠機制
  • 系統級電話黑名單
  • 菜單鍵快速切換應用
重點注意: 7.0對于SDCard的檔案URI的通路做了限制,擷取檔案uri的方式也變了,開發時需要注意。 下面案例會講到。

鴻洋的:Android 7.0 行為變更 通過FileProvider在應用間共享檔案吧

四、8.0新特性

  • 畫中畫
  • 通知标志
  • 自動填充架構
  • 系統優化
  • 背景限制
  • 應用快捷鍵
  • 語言區域和國際化

重點注意:

8.0限制了背景服務這些,啟動背景服務需要設定通知欄,使服務變成前台服務。

8.0對于安裝位置來源的應用做了更嚴格的限制,在app更新安裝時需要做些處理。 下面案例會講到。

Android 8.0 P适配詳細指南

五、9.0新特性

  • 劉海設計
  • 黑白模式切換
  • 加入長截圖
  • 加入護眼模式
  • 通知欄的體驗優化
  • Material Design功能更新等等
重點注意:Android 9.0強制使用https,會阻塞http請求,如果app使用的第三方sdk有http,将全部被阻塞。

六、10 Q 新特性

Q的最大更新就是使用者隐私權限變更。

  • 存儲權限:判斷當應用運作在Q平台上時,通路自己檔案不需申請讀寫權限,通路音頻需要申請新的媒體特定權限。
  • 背景定位權限:如果目标版本targetSDK <= P 請求了ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION權限,Q裝置會自動幫你申請ACCESS_BACKGROUND_LOCATION權限。
  • 背景啟動 Activity:僅針對與使用者毫無互動就啟動一個Activity的情況,比如微信登陸授權。
  • minSDK警告:谷歌要求運作在Q裝置上的應用targetSDK>=Android 6.0(API 級别 23),不然會向使用者發出警告。
  • 裝置辨別符(DeviceId):TelephonyManager.getDeviceId()方法失效。新權限READ_PRIVILEGED_PHONE_STATE隻提供給系統app使用。下面是一個通過硬體資訊生産的UUID。裝置ID的擷取一個版本比一個版本艱難,如有好的方法請指出。
public static String getUUID() {
String serial = null;
 
String m_szDevIDShort = "35" +
        Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
 
        Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
 
        Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
 
        Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
 
        Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
 
        Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
 
        Build.USER.length() % 10; //13 位
 
try {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        serial = android.os.Build.getSerial();
    } else {
        serial = Build.SERIAL;
    }
    //API>=9 使用serial号
    return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
} catch (Exception exception) {
    //serial需要一個初始化
    serial = "serial"; // 随便一個初始化
}
    //使用硬體資訊拼湊出來的15位号碼
    return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}
           
  • 無線和藍牙掃描權限:使用 WLAN API 和 Bluetooth API 時的權限,這兩種權限的變更影響較少。

最後,版本更新踩坑記——DownloadManager

DownloadManager使用介紹參考:https://blog.csdn.net/csdn_aiyang/article/details/64126379

MainActivity.class 代碼

public class MainActivity extends AppCompatActivity {
    private TextView down;
    private TextView progress;
    private ProgressBar pb_update;
    private DownloadManager downloadManager;
    private DownloadManager.Request request;
    public static String downloadUrl = "http://www.wanandroid.com/blogimgs/ecb4c318-42f3-454a-a6c4-615ad16f35bd.apk";

    private DownloadReceiver completeReceiver;
    private final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads");
    private DownloadChangeObserver observer;
    long id;
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Bundle bundle = msg.getData();
            int pro = bundle.getInt("pro");
            pb_update.setProgress(pro);
            progress.setText(String.valueOf(pro) + "%");
        }
    };


    class DownloadChangeObserver extends ContentObserver {

        public DownloadChangeObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            updateView();
        }
    }

    class DownloadReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(final Context context, final Intent intent) {
            Log.i("aaa", "廣播監聽");
        
            long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
            Intent intentInstall = new Intent();
            Uri uri = null;
            if (completeDownLoadId == id) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 相容6.0以下
                   
                    uri = downloadManager.getUriForDownloadedFile(completeDownLoadId);
                    installPackge(context,intentInstall,uri);
                } 
            }
        }
    }

    /**
     * 安裝APK
     * @param context
     * @param intentInstall
     * @param uri
     */
    private void installPackge(Context context,Intent intentInstall,Uri uri){
        intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intentInstall.setAction(Intent.ACTION_VIEW);
        // 安裝應用
        Log.i("aaa", "app下載下傳完成了,開始安裝。。。"+uri);
        intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
        context.startActivity(intentInstall);
    }



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        down = (TextView) findViewById(R.id.down);
        progress = (TextView) findViewById(R.id.progress);
        pb_update = (ProgressBar) findViewById(R.id.pb_update);
        down.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LoadApp();
                //requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,Code_PERMISSION);
            }
        });
    }

    private void LoadApp() {
        //建立下載下傳對象
        downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        request = new DownloadManager.Request(Uri.parse(downloadUrl));
        request.setTitle("app-release.apk");
        request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
        request.setAllowedOverRoaming(false);
        request.setMimeType("application/vnd.android.package-archive");
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);

        //設定檔案存放路徑
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "app-release.apk");
    }

    private void updateView() {
        int[] bytesAndStatus = new int[]{0, 0, 0};
        DownloadManager.Query query = new DownloadManager.Query().setFilterById(id);
        Cursor c = null;
        try {
            c = downloadManager.query(query);
            if (c != null && c.moveToFirst()) {
                //已經下載下傳的位元組數
                bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                //總需下載下傳的位元組數
                bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
            }
        } finally {
            if (c != null) {
                c.close();
            }
        }
        int pro = (bytesAndStatus[0] * 100) / bytesAndStatus[1];
        Message msg = Message.obtain();
        Bundle bundle = new Bundle();
        bundle.putInt("pro", pro);
        msg.setData(bundle);
        handler.sendMessage(msg);
        Log.i("aaa", "下載下傳進度:" + bytesAndStatus[0] + "/" + bytesAndStatus[1]);
    }

    //開始下載下傳
    private void onDownBegin() {
        try {
            id = downloadManager.enqueue(request);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //更新UI
            observer = new DownloadChangeObserver(handler);
            getContentResolver().registerContentObserver(CONTENT_URI, true, observer);
            //安裝
            completeReceiver = new DownloadReceiver();
            registerReceiver(completeReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
            down.setText("正在下載下傳");
            down.setClickable(false);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (observer != null) {
            getContentResolver().unregisterContentObserver(observer);
            unregisterReceiver(completeReceiver);
        }

    }
}
           

布局檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:gravity="center"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dp"
        android:text="下載下傳進度"/>

    <ProgressBar
        android:id="@+id/pb_update"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"
        android:max="100"
        android:progress="10"
        android:layout_marginBottom="20dp"
        />

    <TextView
        android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginBottom="20dp"
        android:text="10%"/>
    <TextView
        android:id="@+id/down"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingTop="10dp"
        android:paddingBottom="10dp"
        android:paddingLeft="30dp"
        android:paddingRight="30dp"
        android:background="@color/colorAccent"
        android:text="立即下載下傳"/>
</LinearLayout>
           

開始運作,進行版本适配。

1、适配6.0 動态權限

targetSdkVersion 23
           

此時,運作報錯:

java.lang.SecurityException: 
No permission to write to /storage/emulated/0/Download/app-release.apk:
Neither user 10484 nor current process has android.permission.WRITE_EXTERNAL_STORAGE.
           

沒有讀寫權限。下面進行添加申請權限代碼:

AndroidManifest.xml 中:

<uses-permission android:name="android.permission.INTERNET" />;
 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>;
           

MainActivity.class 中添加

/**
     * 請求權限
     */
    int Code_PERMISSION = 0;
    /**
     * 權限申請
     * @param ManifestPermission
     * @param CODE
     * @return
     */
    private boolean requestPermission(final String ManifestPermission, final int CODE) {
        //1. 檢查是否已經有該權限
        if (ContextCompat.checkSelfPermission(this,ManifestPermission) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,ManifestPermission)) {
                new AlertDialog.Builder(this)
                        .setTitle("權限申請")
                        .setMessage("親,沒有權限我會崩潰,請把權限賜予我吧!")
                        .setPositiveButton("賞給你", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.cancel();
                                // 使用者同意 ,再次申請
                                ActivityCompat.requestPermissions(MainActivity.this, new String[]{ManifestPermission}, CODE);
                            }
                        })
                        .setNegativeButton("就不給", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.cancel();
                                // 使用者拒絕 ,如果APP必須有權限否則崩潰,那就繼續重複詢問彈框~~
                            }
                        }).show();
            } else {
                //2. 權限沒有開啟,請求權限
                ActivityCompat.requestPermissions(this,
                        new String[]{ManifestPermission}, CODE);
            }

        } else {
            //3. 權限已開,處理邏輯
            return true;
        }
        return false;
    }

    //4. 接收申請成功或者失敗回調
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == Code_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //權限被使用者同意,做相應的事情
                onDownBegin();
            } else {
                //權限被使用者拒絕,做相應的事情
                Toast.makeText(this,"拒絕了權限",Toast.LENGTH_SHORT);
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
           

在點選下載下傳時候進行權限申請。可以執行下載下傳了,但是最後安裝還是報錯了:

Caused by: 
android.content.ActivityNotFoundException:
No Activity found to handle Intent { 
act=android.intent.action.VIEW typ=application/vnd.android.package-archive flg=0x10000000 } 
           
原因是在Android6.0以下和Android6.0上,通過DownloadManager 擷取到的Uri不一樣。差別如下:
  • Android 6.0以下版本:getUriForDownloadedFile得到的為:file:///storage/emulated/0/Android/data/packgeName/files/Download/xxx.apk; 
  •  Android 6.0時:getUriForDownloadedFile得到的值為: content://downloads/my_downloads/10 。
//通過downLoadId查詢下載下傳的apk,解決6.0以後安裝的問題
    public static File queryDownloadedApk(Context context, long downloadId) {
        File targetApkFile = null;
        DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

        if (downloadId != -1) {
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterById(downloadId);
            query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
            Cursor cur = downloader.query(query);
            if (cur != null) {
                if (cur.moveToFirst()) {
                    String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                    if (!TextUtils.isEmpty(uriString)) {
                        targetApkFile = new File(Uri.parse(uriString).getPath());
                    }
                }
                cur.close();
            }
        }
        return targetApkFile;
    }
           

在廣播監聽安裝app處添加6.0-7.0代碼:

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
              uri = downloadManager.getUriForDownloadedFile(completeDownLoadId);
              installPackge(context,intentInstall,uri);
   } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0 - 7.0
                   
              File apkFile = queryDownloadedApk(context, completeDownLoadId);
              uri = Uri.fromFile(apkFile);
              installPackge(context,intentInstall,uri);
   }
           

2、适配Android 7.0 解析包時出現問題

從Android 7.0開始,不再允許在app中把file:// Uri暴露給其他app,否則應用會抛出FileUriExposedException。異常如下:

E/AndroidRuntime: FATAL EXCEPTION: Timer-0
                                                                             
Process: com.demo.aiyang.downloadmanger, PID: 7985
                                                                              
android.os.FileUriExposedException: file:///storage/emulated/0/Download/app-release.apk exposed beyond app through Intent.getData()
           
原因在于,Google認為使用file:// Uri存在一定的風險。比如,檔案是私有的,其他app無法通路該檔案,或者其他app沒有申請READ_EXTERNAL_STORAGE運作時權限。使用FileProvider生成content:// Uri來替代file:// Uri,如下圖:
Android 5.0、6.0、7.0、8.0、9.0 新特性,DownloadManager踩坑記前言正文

使用FileProvider解決上述異常。

(1)聲明FileProvider

首先在Manifest.xml檔案中申明FileProvider。

//記得替換成你項目包名
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="你的包名.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
 
           
  • android:name:固定寫法;
  • android:authorities:可自定義,是用來辨別該provider的唯一辨別;
  • android:exported:必須設定成 false,否則運作時報錯java.lang.SecurityException: Provider must not be exported ;
  • android:grantUriPermissions:用來控制共享檔案的通路權限;
  • <meta-data>節點中的android:resource指定了共享檔案的路徑。

(2) 添加file_paths.xml檔案到res的xml檔案下

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="" />
    <files-path name="files" path="" />
    <cache-path name="cache" path="" />
    <external-path name="external" path="" />
    <external-files-path name="name" path="path" />
     <external-cache-path name="name" path="path" />
</paths>
           
根元素<paths>是固定的,屬性path表示子目錄,内部元素節點如下:
  • <root-path/> 代表裝置的根目錄new File("/");
  • <files-path/> 代表context.getFilesDir()
  • <cache-path/> 代表context.getCacheDir()
  • <external-path/> 代表Environment.getExternalStorageDirectory()
  • <external-files-path>代表context.getExternalFilesDirs()
  • <external-cache-path>代表getExternalCacheDirs()
此處,我們apk下載下傳存放在Sdk外部存儲區目錄下  :
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 該方式提供在應用的外部存儲區根目錄的下的檔案。
   它對應Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)傳回的路徑。
   eg:”/storage/emulated/0/Android/data/com.jph.simple/files”
    -->

    <external-path name="download" path="" />

</paths>
           
上述代碼中path="",是有特殊意義的,它代碼根目錄。如果你将path設為path="pictures",那麼它代表着根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應用分享pictures目錄範圍之外的檔案是不行的。

(3)在廣播監聽APP安裝代碼中添加相容>7.0

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 相容6.0以下
     uri = downloadManager.getUriForDownloadedFile(completeDownLoadId);
    installPackge(context,intentInstall,uri);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 相容6.0-70
    File apkFile = queryDownloadedApk(context, completeDownLoadId);
    uri = Uri.fromFile(apkFile);
    installPackge(context,intentInstall,uri);
} else {//相容7.0
   intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 給目标應用一個臨時授權
   File file= new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS),"app-release.apk");
    
   Uri uri = FileProvider.getUriForFile(context, getPackageName() + ".fileProvider",file);
   installPackge(this,intentInstall,uri);
}
           

其中:

file路徑:/storage/emulated/0/Download/app-release.apk

使用FileProvide得到uri為:content://com.demo.aiyang.demo.fileProvider/download/Download/app-release.apk  

這裡,講一個坑。

在下載下傳成功後,安裝時出錯,如下圖所示

Android 5.0、6.0、7.0、8.0、9.0 新特性,DownloadManager踩坑記前言正文

錯誤擷取File路徑代碼如下: 

//改正前寫法
 File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app-release.apk");
//file路徑:/storage/emulated/0/Android/data/com.demo.aiyang.demo/files/Download/app-release.apk


//改正後寫法
File file= new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"app-release.apk");
//file路徑:/storage/emulated/0/Download/app-release.apk
           

因為FileProvider不支援sdcard目錄下的檔案共享。Android 7.0 DownloadManager與FileProvider的坑

(一把辛酸淚!這個問題解決了很久,試了很多方法,比較坑的是将檔案下載下傳後再寫入到私有目錄!)

3、适配Android 8.0:未知來源的應用權限

Android 8.0的手機會出現線上更新不了新版本,華為榮耀V10手機測試apk下載下傳完成後直接白屏提示“解析包時出現問題”。原因是Android8.0以上未知來源的應用是不可以通過代碼來執行安裝的(允許手動安裝)。Google這麼做是為了防止不合法APK安裝侵犯了使用者權益。适配如下:

(1) 在清單檔案中申明權限

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
           

(2)監聽apk下載下傳狀态的廣播中添加代碼:

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 相容6.0以下
                
                    uri = downloadManager.getUriForDownloadedFile(completeDownLoadId);
                    installPackge(context,intentInstall,uri);
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 相容6.0-70
       
                    File apkFile = queryDownloadedApk(context, completeDownLoadId);
                    uri = Uri.fromFile(apkFile);
                    installPackge(context,intentInstall,uri);
} else {
                    Log.i("aaa", ">7.0");
                    InstallPackgeAPI28(context);// 相容Android 8.0
}
           

相容Android 8.0 是否有安裝權限 

/**
     *  相容 8.0 未知來源應用安裝
     */
    int Code_INSTALLPACKAGES = 1;
    @RequiresApi(api = Build.VERSION_CODES.O)
    private void startInstallPermissionSettingActivity() {
        Uri packageURI = Uri.parse("package:" + getPackageName());
        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
        startActivityForResult(intent, Code_INSTALLPACKAGES);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == Code_INSTALLPACKAGES){
            InstallPackgeAPI28(this);
        }
    }

 private void InstallPackgeAPI28(Context context){
        Intent intentInstall = new Intent();
        intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 給目标應用一個臨時授權
        File file= new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"app-release.apk");
        Uri uri = FileProvider.getUriForFile(context, getPackageName() + ".fileProvider", file);

        boolean isInstallPermission = false;//是否有8.0安裝權限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            isInstallPermission = getPackageManager().canRequestPackageInstalls();
            if (isInstallPermission) {
                installPackge(this,intentInstall,uri);
            } else {
                new AlertDialog.Builder(this)
                        .setTitle("權限申請")
                        .setMessage("親,沒有權限我會崩潰,請把權限賜予我吧!")
                        .setPositiveButton("賞給你", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.cancel();
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                                    startInstallPermissionSettingActivity();
                                }
                            }
                        }).setNegativeButton("取消",null ).show();
            }
        }else{
            installPackge(this,intentInstall,uri);
        }
    }
           

運作結果:

Android 5.0、6.0、7.0、8.0、9.0 新特性,DownloadManager踩坑記前言正文

4、适配Android 9.0:Https網絡請求

前面代碼基本上都已經算非常完整了,可以在大部分手機上都可以正常安裝和使用。但是,我發現依然有很多網友留言說讓我相容9.0。于是乎,我自己也用9.0的測試手機運作了一下,可以下載下傳安裝啊!沒問題啊!

呵呵。。。終究是我太大意了。 demo 裡的  targetSdkVersion  并沒有大于  API 28 Android 9.0 。是以,也就不會出現什麼問題。審視一下一邊代碼就明白了,無非就是無法下載下傳apk的問題。

Android P 9.0 的系統上面預設所有Http的請求都被阻止了,就沒辦法通路了網絡了。解決這個問題最好當然是把Http換成Https了。然鵝,并不是每個公司都做到了。還有一個辦法,就是通過在AnroidManifest.xml中的application标簽下設定如下屬性即可。

android:usesCleartextTraffic="true"
           

完。2019年12月24日16:55:03

歡迎訂閱公衆号:資料結構、算法、面試經驗、每日新聞、閑聊趣文等。歡迎一起學習!

Android 5.0、6.0、7.0、8.0、9.0 新特性,DownloadManager踩坑記前言正文

歡迎加入Android QQ交流學習群:

Android 5.0、6.0、7.0、8.0、9.0 新特性,DownloadManager踩坑記前言正文

參考連結:

https://www.jianshu.com/p/c58d17073e65

https://blog.csdn.net/yq6073025/article/details/52934326

https://www.jianshu.com/p/121bbb07cb07