Android 各版本兼容性適配詳解

Android 6

本文根據我個人的開發經驗,總結瞭從 Android 6 – Android 13 重要的行為變更。當然,這不是 Android 所有的行為變更,這裡隻是列舉瞭我覺得比較有影響的,比較常見的一些場景的開發適配。那麼,下面讓我們一起來瞧瞧,有哪些行為變更是需要我們特別註意的。

在 Android 6 版本開始引進運行時權限機制,Android 將所有的權限歸為兩類,一類是普通權限,一類是危險權限。普通權限一般不會威脅到用戶的安全和隱私,對於這部分權限,系統自動對軟件進行授權,不需要詢問用戶。而危險權限是可能對用戶的安全和隱私造成影響的權限,如獲取設備地理位置、獲取設備聯系人信息等,這些就需要明確通知用戶,並由用戶手動進行授權才可以進行相應操作。

危險權限如下所示:

權限組名 權限名
CALENDAR(日歷) READ_CALENDAR,WRITE_CALENDAR
CAMERA(攝像頭) CAMERA
CONTACTS(聯系人) READ_CONTACTS,WRITE_CONTACTS,GET_ACCOUNTS
LOCATION(定位) ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION
MICROPHONE(麥克風) RECORD_AUDIO
PHONE(手機) READ_PHONE_STATE,CALL_PHONE,READ_CALL_LOG,WRITE_CALL_LOG,ADD_VOICEMAIL,USE_SIP,PROCESS_OUTGOING_CALLS
SENSOR(傳感器) BODY_SENSORS
SMS(短信) SEND_SMS,RECEIVE_SMS,READ_SMS,RECEIVE_WAP_PUSH,RECEIVE_MMS
STORAGE(存儲) READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE

運行時權限動態申請,這裡推薦郭神的開源庫 – PermissionX,使用簡單方便,這裡不再贅述。

Android 7

Android 7 禁止向你的應用外公開 file://URI, 如果在 Android 7 及以上系統傳遞 file:// URI 就會觸發 FileUriExposedException,不適配的話在 Android 7 及以上系統就會出現應用崩潰的現象。如果要在應用間共享文件,可以發送 content://URI 類型的 URI,並授予 URI 臨時訪問權限,這就需要用到 FileProvider 類。

我們以調用系統相機拍照為例,在 res 下創建 xml 目錄,在此目錄下創建 file_paths.xml 文件。

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!-- 內部存儲,對應 filesDir,路徑:/data/data/package_name/files-->
    <files-path
        name="files_path"
        path="." />
    <!-- 內部存儲,對應 cacheDir,路徑:/data/data/package_name/cache-->
    <cache-path
        name="cache_path"
        path="." />
    <!--外部存儲,對應 getExternalFilesDir,路徑:/storage/sdcard/Android/data/package_name/files-->
    <external-files-path
        name="external_files_path"
        path="." />
    <!--外部存儲,對應 externalCacheDir,路徑:/storage/sdcard/Android/data/package_name/cache-->
    <external-cache-path
        name="external_cache_path"
        path="." />
</paths>

在 AndroidManifest 中註冊 FileProvider

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.myapplication.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <!--exported 要為 false,否則會報安全異常,grantUriPermissions 為 true,表示授予 URI 臨時訪問權限-->
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

適配

val pictureFile =
    File(getExternalFilesDir(null), "${System.currentTimeMillis()}.jpg")
if (!pictureFile.exists()) {
    pictureFile.createNewFile()
}
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    // 通過 FileProvider 創建一個 content 類型的 Uri
    val pictureUri =
        FileProvider.getUriForFile(
            this,
            "com.example.myapplication.fileProvider",
            pictureFile
        )
    intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri)
    // 授予目錄臨時共享權限
    intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    startActivity(intent)
} else {
    val pictureUri = Uri.fromFile(pictureFile)
    intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri)
    startActivity(intent)
}

Android 8

從 Android 8 開始,Google 規定所有的通知必須分配一個渠道,每一個渠道,你都可以設置渠道中所有通知的行為。用戶界面將通知渠道稱之為通知類別,用戶可以隨意修改這些設置來決定通知的行為。

val notificationManager =
    getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    val notificationChannel = NotificationChannel(
        "Channel_ID",
        "Channel_Name",
        NotificationManager.IMPORTANCE_DEFAULT
    )
    notificationManager.createNotificationChannel(notificationChannel)
    val notification =
        NotificationCompat.Builder(this, "Channel_ID")
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle("title").setContentText("content").build()
    notificationManager.notify(1, notification)
} else {
    val notification =
        NotificationCompat.Builder(this).setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle("title").setContentText("content").build()
    notificationManager.notify(1, notification)
}

從 Android 8 開始,不允許後臺應用啟動後臺服務,需要使用 startForegroundService 指定為前臺服務,否則系統會停止 Service 並拋出異常。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
val intent = Intent(this, MyService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    startForegroundService(intent)
} else {
    startService(intent)
}
class MyService : Service() {
    override fun onBind(intent: Intent): IBinder? = null
    override fun onCreate() {
        super.onCreate()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val channel = NotificationChannel(
                "Channel_ID", "Channel_Name", NotificationManager.IMPORTANCE_DEFAULT
            )
            manager.createNotificationChannel(channel)
            val notification = Notification.Builder(this, "Channel_ID").build()
            startForeground(1, notification)
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            stopForeground(true)
        }
    }
}

Android 9

從 Android 9 開始,限制瞭 HTTP 網絡請求,如果繼續使用 HTTP 請求,會在日志做出警告,不過隻是無法正常發出請求,不會導致應用崩潰。如果我們需要使用 HTTP 請求的話,需要在 AndroidManifest 中添加如下配置:

<application
    ...
    android:usesCleartextTraffic="true">
    ...
</application>

除瞭這個方法,我們也可以指定域名。在 res 的 xml 目錄下新建文件 network_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">www.wanandroid.com</domain>
    </domain-config>
</network-security-config>

然後在 AndroidManifest 中配置即可

<application
    ...
    android:networkSecurityConfig="@xml/network_config">
    ...
</application>

Android 10

在 Android 10 之前的版本,我們在做文件的操作時都會申請存儲空間的讀寫權限,但是這些權限可能被濫用,造成手機的存儲空間中充斥著大量不明作用的文件,並且應用卸載後也沒刪除掉。為瞭解決這個問題,Android 10 開始引入瞭分區存儲的概念。

分區存儲就是對外部存儲進行瞭重新設計,簡單來說,對於外部共享文件,需要通過 MediaStrore API 和 Storage Access Framework 來訪問,對於外部私有文件,無法讀寫自己應用以外創建的其他文件。

Android 中存儲可以分為兩大類:專屬存儲和共享存儲。

  • 專屬存儲:每個應用在都擁有自己的專屬目錄,其它應用看不到。它包括 APP 自身的內部存儲和外部存儲,這倆無需存儲權限便可訪問。
  • 共享存儲:共享存儲空間存放的是圖片,視頻和音頻等文件,這些資源是公共的,所有 App 都能訪問它們。

舉個例子,如果想拿到共享存儲裡的圖片路徑,該怎麼做呢?

首先需要申請權限,這裡直接使用 PermissionX 。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
PermissionX.init(this)
    .permissions(Manifest.permission.READ_EXTERNAL_STORAGE).request { allGranted, _, _ ->
        if (allGranted) {
            Log.i(tag, "All permissions have been agreed")
        } else {
            Toast.makeText(this, "Please agree to the permission", Toast.LENGTH_SHORT)
                .show()
        }
    }

通過 MediaStrore 查詢

val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.let {
    val indexPhotoPath = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
    while (it.moveToNext()) {
        Log.i(tag, "picture path: ${it.getString(indexPhotoPath)}")
    }
    it.close()
}

媒體文件可以通過 MediaStore 和 SAF 兩種方式訪問,但是非媒體文件隻能通過 SAF 訪問,通過 SAF,用戶可以通過一個簡單的標準界面,以統一的方式瀏覽訪問文件。

這裡以選擇 sdcard 目錄下的一個文本文件,對它進行讀寫操作為例。

private lateinit var startActivity: ActivityResultLauncher&lt;Intent&gt;

在 Activity 中註冊結果返回,這裡需要註意的是,別等到 Activity 的生命周期執行到 onResume 瞭才註冊,會報錯的,建議最好在 onCreate 中進行註冊,在這裡拿到選擇的文件的 uri

startActivity =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.data != null && it.resultCode == Activity.RESULT_OK) {
            readFileContent(it.data!!.data)
        }
    }

寫入內容

private fun writeForUri(uri: Uri?) {
    if (uri == null) return
    try {
        val outputStream = contentResolver.openOutputStream(uri)
        val content = "Hello Android"
        outputStream?.write(content.toByteArray())
        outputStream?.flush()
        outputStream?.close()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

讀取內容

private fun readFileContent(uri: Uri?) {
    if (uri == null) return
    try {
        val inputStream = contentResolver.openInputStream(uri) ?: return
        val readContent = ByteArray(1024)
        var len: Int
        do {
            len = inputStream.read(readContent)
            if (len != -1) { //打印出文件內容
                Log.d(tag, "File Content: ${String(readContent).substring(0, len)}")
            }
        } while (len != -1)
        inputStream.close()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

打開文件選擇器

    private fun openSAF() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        //指定選擇文本類型的文件
        intent.type = "text/plain"
        startActivity.launch(intent)
    }

選擇器的用戶界面是這樣的,那個 NewTextFile.txt 就是我們操作的文本文件。

由此可見,SAF 提供瞭文件選擇器,調用者隻需指定要讀寫的文件類型,比如文本類型,圖片類型,視頻類型等,選擇器就會過濾出相應文件以供選擇,使用簡單。

Android 11

在 Android 11 中,不能直接獲取其他應用的信息瞭,比如,查詢應用信息的代碼如下:

val appList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
for (app in appList) {
    Log.i(tag, "packageName: ${app.packageName}")
}

這段代碼隻能查詢到自己應用和系統應用的信息,如果想要查詢其他應用的信息,需要在 AndroidManifest 中添加對應的包名配置。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    ...
    <queries>
        <package android:name="com.example.composeapp" />
    </queries>
    ...
</manifest>

如果你就是要獲取所有應用的信息,怎麼辦呢?Android 11 也提供瞭 QUERY_ALL_PACKAGES 權限,在 AndroidManifest 中加入即可。但是,加入該權限的時候會有紅線提示,建議使用上面的這種方式。加入該權限的 APP,應用市場能不能過審,就很難說瞭。

Android 11 還增加瞭單次授權,就是請求與位置信息,麥克風或攝像頭相關的權限時,系統會自動提供一個單次授權的選項,隻供這一次權限獲取,選擇它的話,用戶下次再次打開 APP 的時候,系統會再次提示用戶請求權限,所以,需要我們每次使用的時候去判斷一下權限,沒有就去申請。

Android 12

Android 12 增加瞭系統默認的 APP 啟動頁,這個啟動頁會使用 APP 定義的主題生成,這對我們的應用影響還是比較大的,通常我們會用一個 Activity 作為啟動頁來顯示一些廣告推廣啥的,但是在 Android 12 上不適配的話,那用戶將會看到兩個閃屏。怎麼去適配呢? Google 告訴我們,你可以選擇不管或者去掉 SplashActivity 並使用設置主題的方式來兼容,下面來看看設置主題的方式如何去實現?

implementation 'androidx.core:core-splashscreen:1.0.0'
<style name="Theme.App.Splash" parent="Theme.SplashScreen">
    <!--特定的單色填充背景-->
    <item name="windowSplashScreenBackground">@color/white</item>
    <!--起始窗口中心的圖標-->
    <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
    <!--啟動畫面圖標動畫的時長,Google 建議不超過 1000 毫秒-->
    <item name="windowSplashScreenAnimationDuration">200</item>
    <!--必填項,SplashView 移除後使用此主題恢復 Activity 樣式-->
    <item name="postSplashScreenTheme">@style/Theme.MyApplication</item>
</style>

設置主題

<application
    ...
    android:theme="@style/Theme.App.Splash">
    ...
</application>

在啟動 Activity 中調用 installSplashScreen,註意要在 super.onCreate 之前添加。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

另外,Android 12 修改瞭根 Activity 在返回鍵的默認行為。在以前的版本中,返回鍵會執行 finish Activity,而從 Android 12 開始會將任務棧切換到後臺,也就是說在根 Activity 點擊返回鍵時,生命周期隻會執行到 onStop,不執行 onDestroy,所以,用戶返回應用時將執行溫啟動。

Android 13

從 Android 13 開始,用戶可以通過抽屜式通知欄完成工作流,以停止具有持續前臺服務的應用,如下圖所示,此功能稱為前臺服務 (FGS) 任務管理器,應用必須能夠處理這種由用戶發起的停止操作。

此外,Android 13 引入瞭運行時通知權限:POST_NOTIFICATIONS, 如果拒絕這個權限的話,應用將無法發送通知,此更改有助於用戶隻關註自己認為重要的通知,但是與媒體會話以及自行管理通話的應用相關的通知不受此行為變更的影響。

以上就是Android 各版本兼容性適配詳解的詳細內容,更多關於Android 版本兼容性適配的資料請關註WalkonNet其它相關文章!

推薦閱讀: