Android 版本適配:8.x Oreo(API 級別 26、27)

Anlia發表於2019-04-21
版權宣告:本文為博主原創文章,未經博主允許不得轉載
文章分類:Android知識體系 - 版本適配
複製程式碼

一、前言

本文主要是從官方文件中篩選出一些常見的適配項,若有任何紕漏或需要補充的,歡迎大家在評論區指出。

二、版本適配

1. 執行時許可權授予優化

Android 8.0 及以上系統對執行時許可權的授予進行了優化,以下是官方文件的原文:

在 Android 8.0 之前,如果應用在執行時請求許可權並且被授予該許可權,系統會錯誤地將屬於同一許可權組並且在清單中註冊的其他許可權也一起授予應用。

對於針對 Android 8.0 的應用,此行為已被糾正。系統只會授予應用明確請求的許可權。然而,一旦使用者為應用授予某個許可權,則所有後續對該許可權組中許可權的請求都將被自動批准。

例如,假設某個應用在其清單中列出 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE。應用請求 READ_EXTERNAL_STORAGE ,並且使用者授予了該許可權。如果該應用針對的是 API 級別 24 或更低階別,系統還會同時授予 WRITE_EXTERNAL_STORAGE,因為該許可權也屬於同一 STORAGE 許可權組並且也在清單中註冊過。如果該應用針對的是 Android 8.0,則系統此時僅會授予 READ_EXTERNAL_STORAGE;不過,如果該應用後來又請求 WRITE_EXTERNAL_STORAGE,則系統會立即授予該許可權,而不會提示使用者。

也就是說,我們的應用在 Android 8.0 之前,如果在許可權註冊清單中列出 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 許可權:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
複製程式碼

那麼在動態獲取 READ_EXTERNAL_STORAGE 許可權之後,直接使用 WRITE_EXTERNAL_STORAGE 許可權相關的操作時並不會出現任何問題,因為系統已經將同一許可權組並且出現在清單中的其他許可權都一併授予給了我們。

但這一行為在 Android 8.0 及以上版本的系統中就會出現問題,系統會丟擲缺失許可權相關的異常。因此,我們在使用 WRITE_EXTERNAL_STORAGE 許可權相關的操作之前需要再次動態向系統申請許可權,然後系統會自動將許可權授予給我們(由於我們之前已經獲取了同一許可權組中的其他許可權,因此不需要再次彈出視窗讓使用者確認)。

有關許可權組更多的介紹請至官方的許可權說明文件,以下是從文件中擷取的關於許可權分組的表格:

Android 版本適配:8.x Oreo(API 級別 26、27)

2. 安裝未知來源應用

參考資料:Making it safer to get apps on Android O

在 Android 8.0 之前的系統,使用者若從官方應用商店之外的來源安裝應用時,首先需要在系統設定中開啟“允許安裝來自未知來源的應用”選項:

Android 版本適配:8.x Oreo(API 級別 26、27)

這是屬於全域性的設定,開啟之後所有的應用都可以隨意地彈出應用安裝介面來讓使用者安裝。這一特性有可能會被某些惡意應用利用,這些應用為了上架應用市場可能本身並不會攜帶任何惡意程式碼,但它可以彈出一些偽裝成重要安全更新的安裝介面來欺騙使用者,使用者一旦點選了安裝,就會將真正攜帶了惡意程式碼的應用安裝到手機上了。

因此出於安全考慮,谷歌在 Android 8.0 中刪除了這個全域性永久授權的選項,使用者需要對單個應用的“安裝未知來源應用”許可權進行授權:

Android 版本適配:8.x Oreo(API 級別 26、27)

對於開發者來說,若我們的應用中有自動更新安裝的功能,就需要對此進行適配。官方文件中提供了以下方法:

  • 通過PackageManager.canRequestPackageInstalls()方法判斷應用是否擁有REQUEST_INSTALL_PACKAGES許可權(targetSdkVersion 需要大於等於26)

  • 通過跳轉 Action Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES 跳轉至“安裝未知來源應用”授權頁面引導使用者授權REQUEST_INSTALL_PACKAGES許可權

Android 版本適配:8.x Oreo(API 級別 26、27)

完整的適配流程如下:

  • 在 AndroidManifest.xml 中註冊請求安裝的許可權
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
複製程式碼
  • 判斷是否擁有許可權,若未擁有則跳轉至授權介面申請許可權
static final int CODE_MANAGE_UNKNOWN_APP = 100;

public void installApk(Context context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        boolean hasInstallPermission = context.getPackageManager().canRequestPackageInstalls();
        if (!hasInstallPermission) { // 未擁有許可權
            Uri parse = Uri.parse("package:" + context.getPackageName());
            Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, parse);
            startActivityForResult(intent, CODE_MANAGE_UNKNOWN_APP);
        } else { // 擁有許可權
            installApk();
        }
    } else { // 低於 Android 8.0
        installApk();
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    
    if (resultCode == RESULT_OK && requestCode == CODE_MANAGE_UNKNOWN_APP) {
        installApk();
    }
}
複製程式碼

3. 通知渠道

Android 8.0 新增了對通知渠道的支援,具體是指開發者可以自定義應用訊息通知的類別(渠道),這樣使用者就可以在應用的通知管理中根據類別篩選出自己需要的訊息,從而把不想要的訊息遮蔽掉

以高德地圖為例(MIUI系統),其訊息通知共分為了 4 個渠道組,其中每個渠道組下又有不同的渠道類別:

Android 版本適配:8.x Oreo(API 級別 26、27)

這樣使用者就可以根據自己的需求遮蔽掉不想要的訊息通知了:

Android 版本適配:8.x Oreo(API 級別 26、27)

此功能體現在程式碼中的變化就是原來的NotificationCompat.Builder(Context context)構造方法被廢棄,而新的構造方法中多了一個channelId引數:

/**
 * @deprecated use
 * {@link NotificationCompat.Builder#NotificationCompat.Builder(Context, String)} instead.
 * All posted Notifications must specify a NotificationChannel Id.
 */
@Deprecated
public Builder(Context context) {
    this(context, null);
}

public Builder(@NonNull Context context, @NonNull String channelId) {
    ...
}
複製程式碼

channelId即自定義通知渠道的id值,建立通知渠道的簡單實現如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);

    String channelId = "渠道Id";
    String channelName = "渠道名稱";
    int importance = NotificationManager.IMPORTANCE_HIGH;// 通知的重要性級別

    NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
    channel.setDescription("渠道描述");
    channel.enableLights(true);// 是否允許指示燈閃爍
    channel.enableVibration(true);// 是否允許振動
    notificationManager.createNotificationChannel(channel);// 建立通知渠道

    Notification notification = new NotificationCompat.Builder(context, channelId).build();
}
複製程式碼

為通知渠道設定渠道組的簡單實現如下:

String channelGroupId = "渠道組Id";
String channelGroupName = "渠道組名稱";

NotificationChannelGroup channelGroup = new NotificationChannelGroup(channelGroupId, channelGroupName);
notificationManager.createNotificationChannelGroup(channelGroup);// 建立渠道組

channel.setGroup(channelGroupId);
複製程式碼

如果隨著應用版本更新,某些通知渠道的配置需要進行修改,建議刪除原channelId的渠道後重新新建一個渠道,以便一些配置能夠正常生效。刪除通知渠道的方法為:

notificationManager.deleteNotificationChannel(channelId);// 刪除此channelId的通知渠道
複製程式碼

4. 限制隱式廣播的接收

從 Android 8.0 開始,出於節省電量、提升使用者體驗等方面的考慮,自定義以及系統大部分的隱式廣播將無法被靜態註冊的 BroadcastReceiver 接收到。解決的方法如下:

  • 將靜態註冊的 BroadcastReceiver 改為動態註冊

  • 雖然自定義顯示廣播不受此限制,但如果要實現隱式廣播的效果,讓所有註冊接收此廣播的應用都可以順利接收到,那麼可以通過PackageManager.queryBroadcastReceivers()方法來實現:

    Intent broadcastIntent = new Intent();
    broadcastIntent.setAction("自定義廣播Action");
    
    PackageManager packageManager = context.getPackageManager();
    List<ResolveInfo> matchList = packageManager.queryBroadcastReceivers(broadcastIntent, 0);
    for (ResolveInfo resolveInfo : matchList) {
        Intent intent = new Intent();
        intent.setPackage(resolveInfo.activityInfo.applicationInfo.packageName);
        intent.setAction("自定義廣播Action");
        context.sendBroadcast(intent);
    }
    複製程式碼
  • 使用 JobScheduler 替代隱式廣播實現“滿足某個特定條件時去執行某個任務”的功能。

此外,之前提到過系統大部分的隱式廣播會受限制,那麼就意味著仍有小部分不受限制,以下是這些例外的隱式廣播彙總(搬運自官方文件隱式廣播例外):

  • ACTION_LOCKED_BOOT_COMPLETEDACTION_BOOT_COMPLETED,原因:這些廣播只在首次啟動時傳送一次,並且許多應用都需要接收此廣播以便進行作業、鬧鈴等事項的安排。

  • ACTION_USER_INITIALIZEandroid.intent.action.USER_ADDEDandroid.intent.action.USER_REMOVED,原因:這些廣播受特權保護,因此大多數正常應用無論如何都無法接收它們。

  • android.intent.action.TIME_SETACTION_TIMEZONE_CHANGED,原因:時鐘應用可能需要接收這些廣播,以便在時間或時區變化時更新鬧鈴。

  • ACTION_LOCALE_CHANGED,原因:只在語言區域發生變化時傳送,並不頻繁。 應用可能需要在語言區域發生變化時更新其資料。

  • ACTION_USB_ACCESSORY_ATTACHEDACTION_USB_ACCESSORY_DETACHEDACTION_USB_DEVICE_ATTACHEDACTION_USB_DEVICE_DETACHED,原因:如果應用需要了解這些 USB 相關事件的資訊,目前尚未找到能夠替代註冊廣播的可行方案。

  • ACTION_HEADSET_PLUG,原因:由於此廣播只在使用者進行插頭的物理連線或拔出時傳送,因此不太可能會在應用響應此廣播時影響使用者體驗。

  • ACTION_CONNECTION_STATE_CHANGEDACTION_CONNECTION_STATE_CHANGED,原因:與 ACTION_HEADSET_PLUG 類似,應用接收這些藍芽事件的廣播時不太可能會影響使用者體驗。

  • ACTION_CARRIER_CONFIG_CHANGED, TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGEDTelephonyIntents.SECRET_CODE_ACTION,原因:原始裝置製造商 (OEM) 電話應用可能需要接收這些廣播。

  • LOGIN_ACCOUNTS_CHANGED_ACTION,原因:一些應用需要了解登入帳號的變化,以便為新帳號和變化的帳號設定計劃操作。

  • ACTION_PACKAGE_DATA_CLEARED,原因:只在使用者顯式地從 Settings 清除其資料時傳送,因此廣播接收器不太可能嚴重影響使用者體驗。

  • ACTION_PACKAGE_FULLY_REMOVED,原因:一些應用可能需要在另一軟體包被移除時更新其儲存的資料;對於這些應用,尚未找到能夠替代註冊此廣播的可行方案。

  • ACTION_NEW_OUTGOING_CALL,原因:執行操作來響應使用者打電話行為的應用需要接收此廣播。

  • ACTION_DEVICE_OWNER_CHANGED,原因:此廣播傳送得不是很頻繁;一些應用需要接收它,以便知曉裝置的安裝狀態發生了變化。

  • ACTION_EVENT_REMINDER,原因:由日曆提供程式傳送,用於向日歷應用釋出事件提醒。因為日曆提供程式不清楚日曆應用是什麼,所以此廣播必須是隱式廣播。

  • ACTION_MEDIA_MOUNTEDACTION_MEDIA_CHECKINGACTION_MEDIA_UNMOUNTEDACTION_MEDIA_EJECTACTION_MEDIA_UNMOUNTABLEACTION_MEDIA_REMOVEDACTION_MEDIA_BAD_REMOVAL,原因:這些廣播是作為使用者與裝置進行物理互動的結果(安裝或移除儲存卷)或啟動初始化(作為已裝載的可用卷)的一部分傳送的,因此它們不是很常見,並且通常是在使用者的掌控下。

  • SMS_RECEIVED_ACTIONWAP_PUSH_RECEIVED_ACTION,原因:這些廣播依賴於簡訊接收應用。

5. 新的懸浮窗型別

Android 8.0 之前,應用註冊了SYSTEM_ALERT_WINDOW許可權之後,便可以使用以下型別的懸浮窗:

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_OVERLAY
  • TYPE_SYSTEM_ERROR

而在 Android 8.0 及以上系統,若繼續使用以上型別的懸浮窗,就會丟擲android.view.WindowManager$BadTokenException異常。因此若想繼續在其他應用上顯示懸浮窗,就必須使用新的懸浮窗型別TYPE_APPLICATION_OVERLAY,開發者可以通過以下方式進行適配:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}else {
    layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
複製程式碼

相關文章