1. 程式人生 > >Android 8.0(Oreo) 適配

Android 8.0(Oreo) 適配

前陣子,市場中心丟來一個鍋,說華為、360、應用寶要求開發者適配 Android P,否則應用將被不推薦、隱藏甚至下架(華為),從 2018 年 8 月 1 日起,所有向 Google Play 首次提交的新應用都必須針對 Android 8.0 (API 等級 26) 開發; 2018 年 11 月 1 日起,所有 Google Play 的現有應用更新同樣必須針對 Android 8.0。嚇得我趕緊做了下適配,原本覺得應該不難,沒想到過程是曲折的,前途終究還是光明的。

適配的第一步,修改targetSdkVersion為26或以上,然後針對Oreo新的行為變更進行適配。

1. 自適應啟動圖示(非必要)

之前的啟動圖示都是mipmap中的靜態圖片ic_launcher。到後來7.1的時候谷歌開始推廣圓形圖示,在原來android:icon的基礎上又添加了android:roundIcon屬性來讓你的app支援圓形圖示。

                  

到了8.0,情況又變了,如右圖:多了一個mipmap-anydpi-v26資料夾,裡面也是啟動圖,但是不是一張圖片,而是xml檔案。

<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@mipmap/ic_launcher_background"/>
    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

該檔案中主要是設定兩張圖片,一個前景色一個背景色。

其實這個還是按照之前的方式處理,並不會出現什麼特別的問題,主要是在Android原生的ROM桌面圖示顯示有問題,圖示會變得特別大或者被一個白色的圓包裹著。

2. 通知欄

Android 8.0 引入了通知渠道,其允許您為要顯示的每種通知型別建立使用者可自定義的渠道。使用者介面將通知渠道稱之為通知類別。

針對 8.0 的應用,建立通知前需要建立渠道,建立通知時需要傳入 channelId,否則通知將不會顯示。示例程式碼如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //建立通知渠道
            @SuppressLint("WrongConstant")
            NotificationChannel mChannel = new NotificationChannel(channelId, "通知渠道名稱", NotificationManager.IMPORTANCE_DEFAULT);
            mChannel.setDescription("渠道描述");//渠道描述
            mChannel.enableLights(false);//是否顯示通知指示燈
            mChannel.enableVibration(false);//是否振動
            mChannel.setImportance(NotificationManager.IMPORTANCE_HIGH);//通知級別

            NotificationManager notificationManager = (NotificationManager) context.getSystemService(
                    NOTIFICATION_SERVICE);
            notificationManager.createNotificationChannel(mChannel);//建立通知渠道
    
            NotificationCompat.Builder builder = new NotificationCompat.Builder(this,channelId);
        }

channelId對應一類渠道通知,mChannel.setImportance()可以設定通知的重要性。

  • IMPORTANCE_MIN 開啟通知,不會彈出,但沒有提示音,狀態列中無顯示
  • IMPORTANCE_LOW 開啟通知,不會彈出,不發出提示音,狀態列中顯示
  • IMPORTANCE_DEFAULT 開啟通知,不會彈出,發出提示音,狀態列中顯示
  • IMPORTANCE_HIGH 開啟通知,會彈出,發出提示音,狀態列中顯示

3.後臺執行限制

(1)如果針對 Android 8.0 的應用嘗試在不允許其建立後臺服務的情況下使用 startService() 函式,則該函式將引發一個 IllegalStateException。目前我在實際專案中並沒有看到這個Exception的出現,不過為了避免出鍋,我們還是try-catch一下比較靠譜。

try { 
        context.startService(intent);
    } catch (Throwable th) {
        DebugLog.i("service", "start service: " + intent.getComponent() + "error: " + th);
        ExceptionUtils.printExceptionTrace(th);
    }

(2)靜態廣播

  • 針對 Android 8.0的應用無法繼續在其清單中為隱式廣播註冊廣播接收器
  • 應用可以繼續在它們的清單中註冊顯式廣播
  • 應用可以在執行時使用Context.registerReceiver()為任意廣播(不管是隱式還是顯式)註冊接收器
  • 需要簽名許可權的廣播不受此限制所限,因為這些廣播只會傳送到使用相同證書籤名的應用,而不是傳送到裝置上的所有應用

很多人的部落格說,8.0只能在程式碼中註冊傳送,不能在manifest檔案中註冊了,其實不然。在manifest中我們依舊可以註冊,不過在傳送的時候我們需要特殊處理下:

            Intent intent = new Intent();
            intent.setAction(action);
            intent.setComponent(new ComponentName(context.getPackageName(),"receiver的包路徑"));
            context.sendBroadcast(intent);

靜態註冊的時候我們傳送需要新增component,讓廣播知道傳送到哪裡。不過最好還是在程式碼中動態註冊,註冊了要記得取消註冊以免造成記憶體洩漏。

4. 允許安裝未知來源應用

針對 8.0 的應用需要在 AndroidManifest.xml 中宣告 REQUEST_INSTALL_PACKAGES 許可權,否則將無法進行應用內升級。

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

5.許可權

8.0之前你申請讀外部儲存的許可權READ_EXTERNAL_STORAGE,你會自動被賦予寫外部儲存的許可權WRITE_EXTERNAL_STORAGE,因為他們屬於同一組(android.permission-group.STORAGE)許可權,但是現在8.0不一樣了,讀就是讀,寫就是寫,不能混為一談。不過你授予了讀之後,雖然下次還是要申請寫,但是在申請的時候,申請會直接通過,不會讓使用者再授權一次了。

額外篇:Android7.0適配之許可權更改

由於之前也沒有適配7.0的許可權,所以順帶說下7.0適配的問題。

對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含檔案 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。對於這種跳轉到第三方應用的URI需要使用FileProvider進行處理。

String cachePath = getApplicationContext().getExternalCacheDir().getPath();
File picFile = new File(cachePath, "test.jpg");
Uri picUri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, 100);

這是常見的開啟系統相機拍照的程式碼,拍照成功後,照片會儲存在picFile檔案中。

這段程式碼在Android 7.0之前是沒有任何問題,但是如果你嘗試在7.0的系統上執行,會丟擲FileUriExposedException異常。

使用FileProvider

FileProvider使用大概分為以下幾個步驟:

1.manifest中申明FileProvider,android:authorities一般設定為包名+fileProvider。

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="xxx.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

2.res/xml中定義對外暴露的資料夾路徑,即android:resource="@xml/file_paths"

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path path="/storage/emulated/0/" name="files_root" />
    <external-path path="." name="external_storage_root" />
</paths>

在paths節點內部支援以下幾個子節點,分別為:

<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()

每個節點都支援兩個屬性:

name

path

path即為代表目錄下的子目錄,比如:

<external-path

        name="external"

        path="pics" />

代表的目錄即為:Environment.getExternalStorageDirectory()/pics,其他同理。

當這麼宣告以後,程式碼可以使用你所宣告的當前檔案

3.生成content://型別的Uri

        File imagePath = new File(Context.getFilesDir(), "images");
        File newFile = new File(imagePath, "default_image.jpg");
        Uri contentUri

        // 相容Android 7.0版本
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
            outputFileUri = FileProvider.getUriForFile(mContext,BuildConfig.APPLICATION_ID
                    + ".fileProvider",newFile);
        }else {
            outputFileUri = Uri.fromFile(newFile);
        }

4.給Uri授予臨時許可權

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

5.使用Intent傳遞Uri

File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), 
                 "com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
// 授予目錄臨時共享許可權
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
               | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 100);   

許可權變更影響到的功能有:

1.拍照;

new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

2.使用第三方應用開啟檔案或者連結;

new Intent("android.intent.action.VIEW");

3.apk安裝

 /**
     * 安裝apk
     * @param filePath
     */
    public static void installAPK(Context context,String filePath) {
        try {
            boolean isRight = UtilZipCheck.isErrorZip(filePath);
            if (isRight) {
                Intent intent = new Intent(Intent.ACTION_VIEW);

                if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID
                            + ".fileProvider", new File(filePath));
                    intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
                } else {
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive");
                }

                context.startActivity(intent);
            }

        } catch (Exception exception) {
            exception.printStackTrace();
        }

    }