Android 各版本兼容性適配詳解
Android 6
本文根據(jù)我個(gè)人的開發(fā)經(jīng)驗(yàn),總結(jié)了從 Android 6 - Android 13 重要的行為變更。當(dāng)然,這不是 Android 所有的行為變更,這里只是列舉了我覺得比較有影響的,比較常見的一些場景的開發(fā)適配。那么,下面讓我們一起來瞧瞧,有哪些行為變更是需要我們特別注意的。
在 Android 6 版本開始引進(jìn)運(yùn)行時(shí)權(quán)限機(jī)制,Android 將所有的權(quán)限歸為兩類,一類是普通權(quán)限,一類是危險(xiǎn)權(quán)限。普通權(quán)限一般不會(huì)威脅到用戶的安全和隱私,對(duì)于這部分權(quán)限,系統(tǒng)自動(dòng)對(duì)軟件進(jìn)行授權(quán),不需要詢問用戶。而危險(xiǎn)權(quán)限是可能對(duì)用戶的安全和隱私造成影響的權(quán)限,如獲取設(shè)備地理位置、獲取設(shè)備聯(lián)系人信息等,這些就需要明確通知用戶,并由用戶手動(dòng)進(jìn)行授權(quán)才可以進(jìn)行相應(yīng)操作。
危險(xiǎn)權(quán)限如下所示:
權(quán)限組名 | 權(quán)限名 |
---|---|
CALENDAR(日歷) | READ_CALENDAR,WRITE_CALENDAR |
CAMERA(攝像頭) | CAMERA |
CONTACTS(聯(lián)系人) | READ_CONTACTS,WRITE_CONTACTS,GET_ACCOUNTS |
LOCATION(定位) | ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION |
MICROPHONE(麥克風(fēng)) | RECORD_AUDIO |
PHONE(手機(jī)) | 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(存儲(chǔ)) | READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE |
運(yùn)行時(shí)權(quán)限動(dòng)態(tài)申請(qǐng),這里推薦郭神的開源庫 - PermissionX,使用簡單方便,這里不再贅述。
Android 7
Android 7 禁止向你的應(yīng)用外公開 file://URI, 如果在 Android 7 及以上系統(tǒng)傳遞 file:// URI 就會(huì)觸發(fā) FileUriExposedException,不適配的話在 Android 7 及以上系統(tǒng)就會(huì)出現(xiàn)應(yīng)用崩潰的現(xiàn)象。如果要在應(yīng)用間共享文件,可以發(fā)送 content://URI 類型的 URI,并授予 URI 臨時(shí)訪問權(quán)限,這就需要用到 FileProvider 類。
我們以調(diào)用系統(tǒng)相機(jī)拍照為例,在 res 下創(chuàng)建 xml 目錄,在此目錄下創(chuàng)建 file_paths.xml 文件。
<?xml version="1.0" encoding="utf-8"?> <paths> <!-- 內(nèi)部存儲(chǔ),對(duì)應(yīng) filesDir,路徑:/data/data/package_name/files--> <files-path name="files_path" path="." /> <!-- 內(nèi)部存儲(chǔ),對(duì)應(yīng) cacheDir,路徑:/data/data/package_name/cache--> <cache-path name="cache_path" path="." /> <!--外部存儲(chǔ),對(duì)應(yīng) getExternalFilesDir,路徑:/storage/sdcard/Android/data/package_name/files--> <external-files-path name="external_files_path" path="." /> <!--外部存儲(chǔ),對(duì)應(yīng) externalCacheDir,路徑:/storage/sdcard/Android/data/package_name/cache--> <external-cache-path name="external_cache_path" path="." /> </paths>
在 AndroidManifest 中注冊(cè) FileProvider
<provider android:name="androidx.core.content.FileProvider" android:authorities="com.example.myapplication.fileProvider" android:exported="false" android:grantUriPermissions="true"> <!--exported 要為 false,否則會(huì)報(bào)安全異常,grantUriPermissions 為 true,表示授予 URI 臨時(shí)訪問權(quán)限--> <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 創(chuàng)建一個(gè) content 類型的 Uri val pictureUri = FileProvider.getUriForFile( this, "com.example.myapplication.fileProvider", pictureFile ) intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri) // 授予目錄臨時(shí)共享權(quán)限 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 規(guī)定所有的通知必須分配一個(gè)渠道,每一個(gè)渠道,你都可以設(shè)置渠道中所有通知的行為。用戶界面將通知渠道稱之為通知類別,用戶可以隨意修改這些設(shè)置來決定通知的行為。
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 開始,不允許后臺(tái)應(yīng)用啟動(dòng)后臺(tái)服務(wù),需要使用 startForegroundService 指定為前臺(tái)服務(wù),否則系統(tǒng)會(huì)停止 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 網(wǎng)絡(luò)請(qǐng)求,如果繼續(xù)使用 HTTP 請(qǐng)求,會(huì)在日志做出警告,不過只是無法正常發(fā)出請(qǐng)求,不會(huì)導(dǎo)致應(yīng)用崩潰。如果我們需要使用 HTTP 請(qǐng)求的話,需要在 AndroidManifest 中添加如下配置:
<application ... android:usesCleartextTraffic="true"> ... </application>
除了這個(gè)方法,我們也可以指定域名。在 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 之前的版本,我們?cè)谧鑫募牟僮鲿r(shí)都會(huì)申請(qǐng)存儲(chǔ)空間的讀寫權(quán)限,但是這些權(quán)限可能被濫用,造成手機(jī)的存儲(chǔ)空間中充斥著大量不明作用的文件,并且應(yīng)用卸載后也沒刪除掉。為了解決這個(gè)問題,Android 10 開始引入了分區(qū)存儲(chǔ)的概念。
分區(qū)存儲(chǔ)就是對(duì)外部存儲(chǔ)進(jìn)行了重新設(shè)計(jì),簡單來說,對(duì)于外部共享文件,需要通過 MediaStrore API 和 Storage Access Framework 來訪問,對(duì)于外部私有文件,無法讀寫自己應(yīng)用以外創(chuàng)建的其他文件。
Android 中存儲(chǔ)可以分為兩大類:專屬存儲(chǔ)和共享存儲(chǔ)。
- 專屬存儲(chǔ):每個(gè)應(yīng)用在都擁有自己的專屬目錄,其它應(yīng)用看不到。它包括 APP 自身的內(nèi)部存儲(chǔ)和外部存儲(chǔ),這倆無需存儲(chǔ)權(quán)限便可訪問。
- 共享存儲(chǔ):共享存儲(chǔ)空間存放的是圖片,視頻和音頻等文件,這些資源是公共的,所有 App 都能訪問它們。
舉個(gè)例子,如果想拿到共享存儲(chǔ)里的圖片路徑,該怎么做呢?
首先需要申請(qǐng)權(quán)限,這里直接使用 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,用戶可以通過一個(gè)簡單的標(biāo)準(zhǔn)界面,以統(tǒng)一的方式瀏覽訪問文件。
這里以選擇 sdcard 目錄下的一個(gè)文本文件,對(duì)它進(jìn)行讀寫操作為例。
private lateinit var startActivity: ActivityResultLauncher<Intent>
在 Activity 中注冊(cè)結(jié)果返回,這里需要注意的是,別等到 Activity 的生命周期執(zhí)行到 onResume 了才注冊(cè),會(huì)報(bào)錯(cuò)的,建議最好在 onCreate 中進(jìn)行注冊(cè),在這里拿到選擇的文件的 uri
startActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.data != null && it.resultCode == Activity.RESULT_OK) { readFileContent(it.data!!.data) } }
寫入內(nèi)容
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() } }
讀取內(nèi)容
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) { //打印出文件內(nèi)容 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) }
選擇器的用戶界面是這樣的,那個(gè) NewTextFile.txt 就是我們操作的文本文件。
由此可見,SAF 提供了文件選擇器,調(diào)用者只需指定要讀寫的文件類型,比如文本類型,圖片類型,視頻類型等,選擇器就會(huì)過濾出相應(yīng)文件以供選擇,使用簡單。
Android 11
在 Android 11 中,不能直接獲取其他應(yīng)用的信息了,比如,查詢應(yīng)用信息的代碼如下:
val appList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA) for (app in appList) { Log.i(tag, "packageName: ${app.packageName}") }
這段代碼只能查詢到自己應(yīng)用和系統(tǒng)應(yīng)用的信息,如果想要查詢其他應(yīng)用的信息,需要在 AndroidManifest 中添加對(duì)應(yīng)的包名配置。
<?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>
如果你就是要獲取所有應(yīng)用的信息,怎么辦呢?Android 11 也提供了 QUERY_ALL_PACKAGES 權(quán)限,在 AndroidManifest 中加入即可。但是,加入該權(quán)限的時(shí)候會(huì)有紅線提示,建議使用上面的這種方式。加入該權(quán)限的 APP,應(yīng)用市場能不能過審,就很難說了。
Android 11 還增加了單次授權(quán),就是請(qǐng)求與位置信息,麥克風(fēng)或攝像頭相關(guān)的權(quán)限時(shí),系統(tǒng)會(huì)自動(dòng)提供一個(gè)單次授權(quán)的選項(xiàng),只供這一次權(quán)限獲取,選擇它的話,用戶下次再次打開 APP 的時(shí)候,系統(tǒng)會(huì)再次提示用戶請(qǐng)求權(quán)限,所以,需要我們每次使用的時(shí)候去判斷一下權(quán)限,沒有就去申請(qǐng)。
Android 12
Android 12 增加了系統(tǒng)默認(rèn)的 APP 啟動(dòng)頁,這個(gè)啟動(dòng)頁會(huì)使用 APP 定義的主題生成,這對(duì)我們的應(yīng)用影響還是比較大的,通常我們會(huì)用一個(gè) Activity 作為啟動(dòng)頁來顯示一些廣告推廣啥的,但是在 Android 12 上不適配的話,那用戶將會(huì)看到兩個(gè)閃屏。怎么去適配呢? Google 告訴我們,你可以選擇不管或者去掉 SplashActivity 并使用設(shè)置主題的方式來兼容,下面來看看設(shè)置主題的方式如何去實(shí)現(xiàn)?
implementation 'androidx.core:core-splashscreen:1.0.0'
<style name="Theme.App.Splash" parent="Theme.SplashScreen"> <!--特定的單色填充背景--> <item name="windowSplashScreenBackground">@color/white</item> <!--起始窗口中心的圖標(biāo)--> <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item> <!--啟動(dòng)畫面圖標(biāo)動(dòng)畫的時(shí)長,Google 建議不超過 1000 毫秒--> <item name="windowSplashScreenAnimationDuration">200</item> <!--必填項(xiàng),SplashView 移除后使用此主題恢復(fù) Activity 樣式--> <item name="postSplashScreenTheme">@style/Theme.MyApplication</item> </style>
設(shè)置主題
<application ... android:theme="@style/Theme.App.Splash"> ... </application>
在啟動(dòng) Activity 中調(diào)用 installSplashScreen,注意要在 super.onCreate 之前添加。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }
另外,Android 12 修改了根 Activity 在返回鍵的默認(rèn)行為。在以前的版本中,返回鍵會(huì)執(zhí)行 finish Activity,而從 Android 12 開始會(huì)將任務(wù)棧切換到后臺(tái),也就是說在根 Activity 點(diǎn)擊返回鍵時(shí),生命周期只會(huì)執(zhí)行到 onStop,不執(zhí)行 onDestroy,所以,用戶返回應(yīng)用時(shí)將執(zhí)行溫啟動(dòng)。
Android 13
從 Android 13 開始,用戶可以通過抽屜式通知欄完成工作流,以停止具有持續(xù)前臺(tái)服務(wù)的應(yīng)用,如下圖所示,此功能稱為前臺(tái)服務(wù) (FGS) 任務(wù)管理器,應(yīng)用必須能夠處理這種由用戶發(fā)起的停止操作。
此外,Android 13 引入了運(yùn)行時(shí)通知權(quán)限:POST_NOTIFICATIONS, 如果拒絕這個(gè)權(quán)限的話,應(yīng)用將無法發(fā)送通知,此更改有助于用戶只關(guān)注自己認(rèn)為重要的通知,但是與媒體會(huì)話以及自行管理通話的應(yīng)用相關(guān)的通知不受此行為變更的影響。
以上就是Android 各版本兼容性適配詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 版本兼容性適配的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android Button點(diǎn)擊事件的四種實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android Button點(diǎn)擊事件的四種實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android下拉刷新控件SwipeRefreshLayout源碼解析
這篇文章主要為大家詳細(xì)解析Android下拉刷新控件SwipeRefreshLayout源碼,感興趣的小伙伴們可以參考一下2016-07-07Kotlin方法與Lambda表達(dá)式實(shí)踐使用介紹
這篇文章主要介紹了Kotlin方法與Lambda表達(dá)式實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09Android開發(fā)之圖形圖像與動(dòng)畫(四)AnimationListener簡介
就像Button控件有監(jiān)聽器一樣,動(dòng)畫效果也有監(jiān)聽器,只需要實(shí)現(xiàn)AnimationListener就可以實(shí)現(xiàn)對(duì)動(dòng)畫效果的監(jiān)聽,感興趣的朋友可以了解下啊,希望本文對(duì)你有所幫助2013-01-01Android開發(fā)之使用SQLite存儲(chǔ)數(shù)據(jù)的方法分析
這篇文章主要介紹了Android開發(fā)之使用SQLite存儲(chǔ)數(shù)據(jù)的方法,結(jié)合實(shí)例形式分析了Android使用SQLite數(shù)據(jù)庫實(shí)現(xiàn)針對(duì)數(shù)據(jù)的增刪改查操作相關(guān)技巧,需要的朋友可以參考下2017-07-07老生常談ProgressBar、ProgessDialog的用法
下面小編就為大家?guī)硪黄仙U凱rogressBar、ProgessDialog的用法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07Android短信發(fā)送器實(shí)現(xiàn)方法
這篇文章主要介紹了Android短信發(fā)送器實(shí)現(xiàn)方法,以實(shí)例形式較為詳細(xì)的分析了Android短信發(fā)送器從界面布局到功能實(shí)現(xiàn)的完整步驟與相關(guān)技巧,需要的朋友可以參考下2015-09-09WindowManagerService服務(wù)是如何以堆棧的形式來組織窗口
我們知道,在Android系統(tǒng)中,Activity是以堆棧的形式組織在ActivityManagerService服務(wù)中的;在本文中,我們就詳細(xì)分析WindowManagerService服務(wù)是如何以堆棧的形式來組織窗口的2013-01-01