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

版權聲明:本文爲博主原創文章,未經博主容許不得轉載
文章分類:Android知識體系 - 版本適配
複製代碼

1、前言

本文主要是從官方文檔中篩選出一些常見的適配項,如有任何紕漏或須要補充的,歡迎你們在評論區指出。html

2、版本適配

1. 運行時權限授予優化

Android 8.0 及以上系統對運行時權限的授予進行了優化,如下是官方文檔的原文:android

在 Android 8.0 以前,若是應用在運行時請求權限而且被授予該權限,系統會錯誤地將屬於同一權限組而且在清單中註冊的其餘權限也一塊兒授予應用。安全

對於針對 Android 8.0 的應用,此行爲已被糾正。系統只會授予應用明確請求的權限。然而,一旦用戶爲應用授予某個權限,則全部後續對該權限組中權限的請求都將被自動批准。bash

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

也就是說,咱們的應用在 Android 8.0 以前,若是在權限註冊清單中列出 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 權限:ide

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

那麼在動態獲取 READ_EXTERNAL_STORAGE 權限以後,直接使用 WRITE_EXTERNAL_STORAGE 權限相關的操做時並不會出現任何問題,由於系統已經將同一權限組而且出如今清單中的其餘權限都一併授予給了咱們。post

但這一行爲在 Android 8.0 及以上版本的系統中就會出現問題,系統會拋出缺失權限相關的異常。所以,咱們在使用 WRITE_EXTERNAL_STORAGE 權限相關的操做以前須要再次動態向系統申請權限,而後系統會自動將權限授予給咱們(因爲咱們以前已經獲取了同一權限組中的其餘權限,所以不須要再次彈出窗口讓用戶確認)。優化

有關權限組更多的介紹請至官方的權限說明文檔,如下是從文檔中截取的關於權限分組的表格:ui

2. 安裝未知來源應用

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

在 Android 8.0 以前的系統,用戶若從官方應用商店以外的來源安裝應用時,首先須要在系統設置中打開「容許安裝來自未知來源的應用」選項:

這是屬於全局的設置,打開以後全部的應用均可以隨意地彈出應用安裝界面來讓用戶安裝。這一特性有可能會被某些惡意應用利用,這些應用爲了上架應用市場可能自己並不會攜帶任何惡意代碼,但它能夠彈出一些假裝成重要安全更新的安裝界面來欺騙用戶,用戶一旦點擊了安裝,就會將真正攜帶了惡意代碼的應用安裝到手機上了。

所以出於安全考慮,谷歌在 Android 8.0 中刪除了這個全局永久受權的選項,用戶須要對單個應用的「安裝未知來源應用」權限進行受權:

對於開發者來講,若咱們的應用中有自動更新安裝的功能,就須要對此進行適配。官方文檔中提供瞭如下方法:

  • 經過PackageManager.canRequestPackageInstalls()方法判斷應用是否擁有REQUEST_INSTALL_PACKAGES權限(targetSdkVersion 須要大於等於26)

  • 經過跳轉 Action Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES 跳轉至「安裝未知來源應用」受權頁面引導用戶受權REQUEST_INSTALL_PACKAGES權限

完整的適配流程以下:

  • 在 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 個渠道組,其中每一個渠道組下又有不一樣的渠道類別:

這樣用戶就能夠根據本身的需求屏蔽掉不想要的消息通知了:

此功能體如今代碼中的變化就是原來的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
}
複製代碼
相關文章
相關標籤/搜索