1. 程式人生 > >Android 8.0 適配

Android 8.0 適配

Android 8.0  Android O Api 26

8.0適配主要是以下7個方面:

1.自適應啟動圖示,讓app支援圓形圖示

參考這裡就夠了郭霖的專欄

2.動態許可權申請

例子:比如你申請了讀sd卡許可權,在8.0以前,你還可以寫sd卡。

            但是在8.0之後,你必須重新申請寫sd卡。否則會異常。雖然再申請的時候,不會彈出框,但是必須要這麼操作

總結:用什麼許可權就去申請什麼許可權,否則會fc。如果你只申請了許可權組中的某些許可權,卻用了同組的其他許可權,那麼你就需要去適配一下了

3.通知適配

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

// 建立通知渠道
private void initNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        CharSequence name = mContext.getString(R.string.app_name);
        NotificationChannel channel = new NotificationChannel(mChannelId, name, NotificationManager.IMPORTANCE_DEFAULT);
        mNotificationManager.createNotificationChannel(channel);
    }
}
// 建立通知傳入channelId
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationBarManager.getInstance().getChannelId());

我們是對8.0單獨適配的,所以無論是NotificationChannelGroup還是NotificationChannel都要與其他版本區分使用,因此我們時不時的要加上這個判斷

if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT)

意思是要不低於8.0的時候才使用

4.後臺執行限制較重要

方案一應用處於後臺時,雖然不能通過startService建立後臺服務,但仍可以通過下面的方式建立前臺服務。

NotificationManager noti = (NotificationManager)getApplicationContext().getSystemService(NOTIFICATION_SERVICE);
noti.startServiceInForeground();

方案二考拉app:

我們無法得知系統如何判斷是否允許應用建立後臺服務,所以我們目前只能簡單 try-catch startService(),保證應用不會 crash,示例程式碼:

Intent intent = new Intent(getApplicationContext(), InitializeService.class);
intent.setAction(InitializeService.INITIALIZE_ACTION);
intent.putExtra(InitializeService.EXTRA_APP_INITIALIZE, appInitialize);
ServiceUtils.safeStartService(mApplication, intent);

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

5.應用內升級(允許安裝未知來源的應用)

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

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

6.主題activity設定螢幕方向

針對 8.0 的應用,設定了透明主題的Activity,再設定螢幕方向,程式碼如下:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:windowIsTranslucent">true</item>
</style>

<activity
    android:name=".MainActivity"
    android:screenOrientation="portrait"
    android:theme="@style/AppTheme">
</activity>

將會丟擲以下異常:

java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation

大概意思是:只有不透明的全屏Activity可以自主設定介面方向.

即使滿足上述條件,該異常也並非一定會出現,為什麼這麼說,看下面兩種表現:

  • targetSdk=26,滿足上述條件,API 26 手機沒問題,API 27 手機沒問題

  • targetSdk=27,滿足上述條件,API 26 手機Crash,API 27 手機沒問題

有點摸不清 Google 的套路了……

可知,targetSdk=26 時,API 26 和 27 都沒有問題,所以這個坑暫時放在適配 API 27 時再填吧。

7.廣播適配

每次傳送廣播時,應用的接收器都會消耗資源。 如果多個應用註冊了接收基於系統事件的廣播,這會引發問題;觸發廣播的系統事件會導致所有應用快速地連續消耗資源,從而降低使用者體驗。為了緩解這一問題,Android 7.0(API級別25)對廣播施加了一些限制,而Android 8.0讓這些限制更為嚴格。

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

解決的辦法:

    1.使用動態廣播替換靜態廣播

    2.保留原來的靜態廣播,但需完善component引數

8.懸浮窗適配(暫時沒有用到)

還記得各種安全衛士在桌面的那個小火箭小娃娃懸浮窗吧,我們最早是把WindowManager.LayoutParams的type設定為TYPE_SYSTEM_ALERT實現。隨後大神們嘗試用TYPE_PHONE與TYPE_TOAST來繞過系統限制(詳見UCToast)。但是怎麼說呢,這些畢竟算旁門左道吧,官方早晚會把這個漏洞堵住的,這不Android 8.0又開始各種android.view.WindowManager$BadTokenException: Unable to add window了 我們看看文件描述:如果應用使用SYSTEM_ALERT_WINDOW許可權並且嘗試使用以下視窗型別之一來在其他應用和系統視窗上方顯示提醒視窗: TYPE_PHONE、 TYPE_PRIORITY_PHONE、 TYPE_SYSTEM_ALERT、 TYPE_SYSTEM_OVERLAY、 TYPE_SYSTEM_ERROR ...那麼,這些視窗將始終顯示在使用 TYPE_APPLICATION_OVERLAY 視窗型別的視窗下方 如果應用針對的是Android 8.0,則應用會使用TYPE_APPLICATION_OVERLAY視窗型別來顯示提醒視窗 這樣就明白了吧,TYPE_APPLICATION_OVERLAY是最上層顯示視窗

來看看程式碼,首先配置一下許可權,為了兩種方式都滿足

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

記得在6.0之後去申請懸浮許可權,首先要判斷許可權是否已經授予 在4.4以前是不用判斷懸浮窗許可權的直接使用就可以了。在4.4到6.0之前,google沒有提供方法讓我們用於判斷懸浮窗許可權,同時也沒有跳轉到設定介面進行開啟的方法,因為此許可權是預設開啟的,但是有一些產商會修改它,所以在使用之前最好進行判斷,以免使用時出現崩潰,判斷方法是用反射的方式獲取出是否開啟了懸浮窗許可權。在6.0以及以後的版本中,google為我們提供了判斷方法和跳轉介面的方法,直接使用Settings.canDrawOverlays(context)就可以判斷是否開啟了懸浮窗許可權,沒有開啟可以跳轉到設定介面讓使用者開啟

private fun checkFloatPermission() : Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val appOpsMgr = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
        val mode = appOpsMgr.checkOpNoThrow("android:system_alert_window", android.os.Process.myUid(), packageName)
return mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_IGNORED
    }
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
        return Settings.canDrawOverlays(this)
    }
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
        var cls = Class.forName("android.content.Context")
        val declaredField = cls.getDeclaredField("APP_OPS_SERVICE")
        declaredField.isAccessible = true
        var obj = declaredField.get(cls) as? String ?: return false
        val str2 = obj
        obj = cls.getMethod("getSystemService", String::class.java).invoke(this, str2)  as String
        cls = Class.forName("android.app.AppOpsManager")
        val declaredField2 = cls.getDeclaredField("MODE_ALLOWED")
        declaredField2.isAccessible = true
        val checkOp = cls.getMethod("checkOp", Integer.TYPE, Integer.TYPE, String::class.java)
        val result = checkOp.invoke(obj, 24, Binder.getCallingUid(), packageName) as Int
        return result == declaredField2.getInt(cls)
    }
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {        return true
    }
    return false
}

這裡我只判斷6.0以後的跳轉,其他版本的跳轉方法自行查詢

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if(!Settings.canDrawOverlays(applicationContext)) {
        val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName))
        startActivity(intent)
        return
    }
}

準備就緒之後即可通過程式碼設定type

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
else {
    mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}