Android 10 適配攻略,你適配了嗎?

相比較去年的寫(xiě)的Android 9適配,這次Android 10的內(nèi)容有點(diǎn)多。沒(méi)想到寫(xiě)了我整整兩天,吐血中。。。

老規(guī)矩,首先將我們項(xiàng)目中的targetSdkVersion改為 29。

1、Scoped Storage(分區(qū)存儲(chǔ))

說(shuō)明

在Android 10之前的版本上,我們?cè)谧鑫募牟僮鲿r(shí)都會(huì)申請(qǐng)存儲(chǔ)空間的讀寫(xiě)權(quán)限。但是這些權(quán)限完全被濫用,造成的問(wèn)題就是手機(jī)的存儲(chǔ)空間中充斥著大量不明作用的文件,并且應(yīng)用卸載后它也沒(méi)有刪除掉。

為了解決這個(gè)問(wèn)題,Android 10 中引入了Scoped Storage 的概念,通過(guò)添加外部存儲(chǔ)訪問(wèn)限制來(lái)實(shí)現(xiàn)更好的文件管理。

首先明確一個(gè)概念,外部?jī)?chǔ)存內(nèi)部?jī)?chǔ)存。

內(nèi)部?jī)?chǔ)存:/data 目錄。一般我們使用getFilesDir() 或 getCacheDir() 方法獲取本應(yīng)用的內(nèi)部?jī)?chǔ)存路徑,讀寫(xiě)該路徑下的文件不需要申請(qǐng)儲(chǔ)存空間讀寫(xiě)權(quán)限,且卸載應(yīng)用時(shí)會(huì)自動(dòng)刪除。

外部?jī)?chǔ)存:/storage 或 /mnt 目錄。一般我們使用getExternalStorageDirectory()方法獲取的路徑來(lái)存取文件。

因?yàn)椴煌瑥S商、系統(tǒng)版本的原因,所以上述的方法并沒(méi)有一個(gè)固定的文件路徑。了解了上面的概念,那我們所說(shuō)的外部?jī)?chǔ)存訪問(wèn)限制,可以認(rèn)為是針對(duì)getExternalStorageDirectory()路徑下的文件。

具體的規(guī)則如下表:

上圖將外部存儲(chǔ)空間分為了三部分:

  • 特定目錄(App-specific),使用getExternalFilesDir()或 getExternalCacheDir()方法訪問(wèn)。無(wú)需權(quán)限,且卸載應(yīng)用時(shí)會(huì)自動(dòng)刪除。

  • 照片、視頻、音頻這類(lèi)媒體文件。使用MediaStore 訪問(wèn),訪問(wèn)其他應(yīng)用的媒體文件時(shí)需要READ_EXTERNAL_STORAGE權(quán)限。

  • 其他目錄,使用存儲(chǔ)訪問(wèn)框架SAF(Storage Access Framwork)
    https://developer.android.google.cn/guide/topics/providers/document-provider?hl=zh_cn

所以在Android 10上即使你擁有了儲(chǔ)存空間的讀寫(xiě)權(quán)限,也無(wú)法保證可以正常的進(jìn)行文件的讀寫(xiě)操作。

適配

最簡(jiǎn)單粗暴的方法就是在AndroidManifest.xml中添加android:requestLegacyExternalStorage="true"來(lái)請(qǐng)求使用舊的存儲(chǔ)模式。

但是我不推薦此方法。

因?yàn)樵谙乱粋€(gè)版本的Android中,此條配置將會(huì)失效,將強(qiáng)制采用外部?jī)?chǔ)存限制。其實(shí)早在Android Q Beta 3之前都是強(qiáng)制的,但為了給開(kāi)發(fā)者適配的時(shí)間才沒(méi)有強(qiáng)制執(zhí)行。所以如果你不抓住這段時(shí)間去適配,那么今年下半年出了Android 11。。。直接開(kāi)花~~

如果你已經(jīng)適配Android 10,這里有個(gè)現(xiàn)象要注意一下:

如果應(yīng)用通過(guò)升級(jí)安裝,那么還會(huì)使用以前的儲(chǔ)存模式(Legacy View)。只有通過(guò)首次安裝或是卸載重新安裝才能啟用新模式(Filtered View)。

所以在適配時(shí),我們的判斷代碼如下:

    // 使用Environment.isExternalStorageLegacy()來(lái)檢查APP的運(yùn)行模式
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && 
       !Environment.isExternalStorageLegacy()) {

    }

這樣的好處是你可以在用戶升級(jí)后,能方便的將用戶的數(shù)據(jù)移動(dòng)至應(yīng)用的特定目錄。

否則你只能通過(guò)SAF去移動(dòng),這樣會(huì)非常麻煩。如果你要移動(dòng)數(shù)據(jù)注意只適用于Android 10下,所以現(xiàn)在適配反而是一個(gè)好時(shí)機(jī)。

當(dāng)然如果你不需要遷移數(shù)據(jù),那適配會(huì)更省事。

下面就說(shuō)說(shuō)推薦適配方案:

對(duì)于應(yīng)用中涉及的文件操作,修改一下你的文件路徑。

以前我們習(xí)慣使用Environment.getExternalStorageDirectory()方法,那么現(xiàn)在可以使用getExternalFilesDir()方法(包括下載的安裝包這類(lèi)的文件)。如果是緩存類(lèi)型文件,可以放到getExternalCacheDir()路徑下。

或者使用MediaStore,將文件存至對(duì)應(yīng)的媒體類(lèi)型中(圖片:MediaStore.Images ,視頻:MediaStore.Video,音頻:MediaStore.Audio),不過(guò)僅限于多媒體文件。

下面代碼將圖片保存到公共目錄下,返回Uri:

   public static Uri createImageUri(Context context) {
        ContentValues values = new ContentValues();
        // 需要指定文件信息時(shí),非必須
        values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
        values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png");
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
        values.put(MediaStore.Images.Media.TITLE, "Image.png");
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");

        return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }

對(duì)于媒體資源的訪問(wèn):比如圖片選擇器這類(lèi)的場(chǎng)景。無(wú)法直接使用File,而應(yīng)使用Uri。否則報(bào)錯(cuò)如下:

java.io.FileNotFoundException: open failed: EACCES (Permission denied)

比如我在適配項(xiàng)目中使用的圖片選擇器時(shí),首先修改了Glide 通過(guò)加載File的方式顯示圖片。改為加載Uri的方式,否則圖片無(wú)法顯示出來(lái)。

Uri的獲取方式還是使用MediaStore:

String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));

Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

其次為了便于不影響之前選擇圖片返回File的邏輯(因?yàn)橐话愣际巧蟼鱂ile,沒(méi)有直接上傳Uri的操作),所以我將最終選擇的文件又轉(zhuǎn)存進(jìn)了getExternalFilesDir(),主要代碼如下:

    File imgFile = this.getExternalFilesDir("image");
    if (!imgFile.exists()){
        imgFile.mkdir();
    }
    try {
        File file = new File(imgFile.getAbsolutePath() + File.separator + 
            System.currentTimeMillis() + ".jpg");
        // 使用openInputStream(uri)方法獲取字節(jié)輸入流
        InputStream fileInputStream = getContentResolver().openInputStream(uri);
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        byte[] buffer = new byte[1024];
        int byteRead;
        while (-1 != (byteRead = fileInputStream.read(buffer))) {
            fileOutputStream.write(buffer, 0, byteRead);
        }
        fileInputStream.close();
        fileOutputStream.flush();
        fileOutputStream.close();
        // 文件可用新路徑 file.getAbsolutePath()
    } catch (Exception e) {
        e.printStackTrace();        
    }

如果你要獲取圖片中的地理位置信息,需要申請(qǐng)ACCESS_MEDIA_LOCATION權(quán)限,并使用MediaStore.setRequireOriginal()獲取。下面是官方的示例代碼:

Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
     cursor.getString(idColumnIndex));

final double[] latLong;

// 從ExifInterface類(lèi)獲取位置信息
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
    ExifInterface exifInterface = new ExifInterface(stream);
    double[] returnedLatLong = exifInterface.getLatLong();

    // If lat/long is null, fall back to the coordinates (0, 0).
    latLong = returnedLatLong != null ? returnedLatLong : new double[2];

    // Don't reuse the stream associated with the instance of "ExifInterface".
    stream.close();
} else {
    // Failed to load the stream, so return the coordinates (0, 0).
    latLong = new double[2];
}

這樣下來(lái),一個(gè)圖片選擇器就基本適配完了。

補(bǔ)充

應(yīng)用在卸載后,會(huì)將App-specific目錄下的數(shù)據(jù)刪除,如果在AndroidManifest.xml中聲明:android:hasFragileUserData="true"用戶可以選擇是否保留。

對(duì)于SAF的使用,可以查看我之前寫(xiě)的SAF使用攻略,這里就不展開(kāi)說(shuō)了。

https://weilu.blog.csdn.net/article/details/104199446

最后這里有一個(gè)介紹Scoped Storage的視頻,推薦觀看:

https://www.bilibili.com/video/av77198618

2、權(quán)限變化

從6.0開(kāi)始,基本每次都會(huì)有權(quán)限方面變動(dòng),這次也不例外。

(前幾天發(fā)布了Android 11的預(yù)覽版,看來(lái)也有權(quán)限方面的變化。。。單次權(quán)限即將到來(lái))

1、在后臺(tái)運(yùn)行時(shí)訪問(wèn)設(shè)備位置信息需要權(quán)限

Android 10 引入了 ACCESS_BACKGROUND_LOCATION 權(quán)限(危險(xiǎn)權(quán)限)。

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

該權(quán)限允許應(yīng)用程序在后臺(tái)訪問(wèn)位置。如果請(qǐng)求此權(quán)限,則還必須請(qǐng)求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION權(quán)限。只請(qǐng)求此權(quán)限無(wú)效果。

在Android 10的設(shè)備上,如果你的應(yīng)用的 targetSdkVersion < 29,則在請(qǐng)求ACCESS_FINE_LOCATION 或ACCESS_COARSE_LOCATION權(quán)限時(shí),系統(tǒng)會(huì)自動(dòng)同時(shí)請(qǐng)求ACCESS_BACKGROUND_LOCATION。

在請(qǐng)求彈框中,選擇“始終允許”表示同意后臺(tái)獲取位置信息,選擇“僅在應(yīng)用使用過(guò)程中允許”或"拒絕"選項(xiàng)表示拒絕授權(quán)。

如果你的應(yīng)用的 targetSdkVersion >= 29,則請(qǐng)求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION權(quán)限表示在前臺(tái)時(shí)擁有訪問(wèn)設(shè)備位置信息的權(quán)。在請(qǐng)求彈框中,選擇“始終允許”表示前后臺(tái)都可以獲取位置信息,選擇“僅在應(yīng)用使用過(guò)程中允許”只表示擁有前臺(tái)的權(quán)限。

總結(jié)一下就是下圖:

其實(shí)官方不推薦你使用申請(qǐng)后臺(tái)訪問(wèn)權(quán)的方式,因?yàn)檫@樣的結(jié)果無(wú)非就是多請(qǐng)求一個(gè)權(quán)限,那么這像變更還有什么意義?申請(qǐng)過(guò)多的權(quán)限,也會(huì)造成用戶的反感。所以官方推薦使用前臺(tái)服務(wù)來(lái)實(shí)現(xiàn),在前臺(tái)服務(wù)中獲取位置信息。

1. 首先在清單中對(duì)應(yīng)的service中添加 android:foregroundServiceType="location":

<service
    android:name="MyNavigationService"
    android:foregroundServiceType="location" ... >
    ...
</service>

2. 啟動(dòng)前臺(tái)服務(wù)前檢查是否具有前臺(tái)的訪問(wèn)權(quán)限:

    boolean permissionApproved = ActivityCompat.checkSelfPermission(this, 
        Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;

    if (permissionApproved) {
       // 啟動(dòng)前臺(tái)服務(wù)
    } else {
       // 請(qǐng)求前臺(tái)訪問(wèn)位置權(quán)限
    }

如此一來(lái)就可以在Service中獲取位置信息。

2、一些電話、藍(lán)牙和WLAN的API需要精確位置權(quán)限

下面列舉了Android 10中必須具有 ACCESS_FINE_LOCATION 權(quán)限才能使用類(lèi)和方法:

電話

TelephonyManager

  • getCellLocation()
  • getAllCellInfo()
  • requestNetworkScan()
  • requestCellInfoUpdate()
  • getAvailableNetworks()
  • getServiceState()
  • TelephonyScanManager
  • requestNetworkScan()
  • TelephonyScanManager.NetworkScanCallback
  • onResults()
  • PhoneStateListener
  • onCellLocationChanged()
  • onCellInfoChanged()
  • onServiceStateChanged()

WLAN

  • WifiManager
  • startScan()
  • getScanResults()
  • getConnectionInfo()
  • getConfiguredNetworks()
  • WifiAwareManager
  • WifiP2pManager
  • WifiRttManager

藍(lán)牙

  • BluetoothAdapter
  • startDiscovery()
  • startLeScan()
  • BluetoothAdapter.LeScanCallback
  • BluetoothLeScanner
  • startScan()

我們可以根據(jù)上面提供的具體類(lèi)和方法,在適配項(xiàng)目中檢查是否有使用到并及時(shí)處理。

3、ACCESS_MEDIA_LOCATION

Android 10新增權(quán)限,上面有提到,不贅述了。

4、PROCESS_OUTGOING_CALLS

Android 10上該權(quán)限已廢棄。

3、后臺(tái)啟動(dòng) Activity 的限制

簡(jiǎn)單解釋就是應(yīng)用處于后臺(tái)時(shí),無(wú)法啟動(dòng)Activity。

比如點(diǎn)開(kāi)一個(gè)應(yīng)用會(huì)進(jìn)入啟動(dòng)頁(yè)或者廣告頁(yè),一般會(huì)有幾秒的延時(shí)再跳轉(zhuǎn)至首頁(yè)。如果這期間你退到后臺(tái),那么你將無(wú)法看到跳轉(zhuǎn)過(guò)程。而在之前的版本中,會(huì)強(qiáng)制彈出頁(yè)面至前臺(tái)。

既然是限制,那么肯定有不受限的情況,主要有以下幾點(diǎn):

  • 應(yīng)用具有可見(jiàn)窗口,例如前臺(tái) Activity。
  • 應(yīng)用在前臺(tái)任務(wù)的返回棧中已有的 Activity。
  • 應(yīng)用在 Recents 上現(xiàn)有任務(wù)的返回棧中已有的 Activity。Recents 就是我們的任務(wù)管理列表。
  • 應(yīng)用收到系統(tǒng)的 PendingIntent 通知。
  • 應(yīng)用收到它應(yīng)該在其中啟動(dòng)界面的系統(tǒng)廣播。示例包括 ACTION_NEW_OUTGOING_CALL 和 SECRET_CODE_ACTION。應(yīng)用可在廣播發(fā)送幾秒鐘后啟動(dòng) Activity。

用戶已向應(yīng)用授予 SYSTEM_ALERT_WINDOW 權(quán)限,或是在應(yīng)用權(quán)限頁(yè)開(kāi)啟后臺(tái)彈出頁(yè)面的開(kāi)關(guān)。

因?yàn)榇隧?xiàng)行為變更適用于在 Android 10 上運(yùn)行的所有應(yīng)用,所以這一限制導(dǎo)致最明顯的問(wèn)題就是點(diǎn)擊推送信息時(shí),有些應(yīng)用無(wú)法進(jìn)行正常的跳轉(zhuǎn)(具體的實(shí)現(xiàn)問(wèn)題導(dǎo)致)。所以針對(duì)這類(lèi)問(wèn)題,可以采取PendingIntent的方式,發(fā)送通知時(shí)使用setContentIntent方法。

當(dāng)然你也可以申請(qǐng)相應(yīng)權(quán)限或者白名單:

不過(guò)申請(qǐng)白名單這種方法受各種手機(jī)廠商所限,很麻煩。感覺(jué)還不如引導(dǎo)用戶手動(dòng)開(kāi)啟權(quán)限。。。

對(duì)于全屏 intent,注意設(shè)置最高優(yōu)先級(jí)和添加USE_FULL_SCREEN_INTENT權(quán)限,這是一個(gè)普通權(quán)限。比如微信來(lái)語(yǔ)音或者視頻通話時(shí),彈出的接聽(tīng)頁(yè)面就是使用這一功能。

    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
Intent fullScreenIntent = new Intent(this, CallActivity.class);
PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
        fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Builder notificationBuilder =
        new NotificationCompat.Builder(this, CHANNEL_ID)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle("Incoming call")
    .setContentText("(919) 555-1234")
    .setPriority(NotificationCompat.PRIORITY_HIGH) // <--- 高優(yōu)先級(jí)
    .setCategory(NotificationCompat.CATEGORY_CALL)

    // Use a full-screen intent only for the highest-priority alerts where you
    // have an associated activity that you would like to launch after the user
    // interacts with the notification. Also, if your app targets Android 10
    // or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
    // order for the platform to invoke this notification.
    .setFullScreenIntent(fullScreenPendingIntent, true); // <--- 全屏 intent

Notification incomingCallNotification = notificationBuilder.build();

注意:在部分手機(jī)上,直接設(shè)置setPriority無(wú)效(或者說(shuō)以渠道優(yōu)先級(jí)為準(zhǔn))。所以需要?jiǎng)?chuàng)建通知渠道時(shí)將重要性設(shè)置為IMPORTANCE_HIGH。

NotificationChannel channel = new NotificationChannel(channelId, "xxx", NotificationManager.IMPORTANCE_HIGH);

后臺(tái)啟動(dòng) Activity 的限制的目的是為了減少對(duì)用戶操作的中斷。如果你有要彈出的頁(yè)面,推薦你先彈出通知,讓用戶自己選擇接下來(lái)的操作,而不是一股腦的強(qiáng)制彈出。(如果你的全屏intent都讓用戶反感,那他也可以關(guān)掉你的通知,不至于任你擺布。)

4、深色主題

Android 10 新增了一個(gè)系統(tǒng)級(jí)的深色主題(在系統(tǒng)設(shè)置中開(kāi)啟)。雖然深色主題并不是強(qiáng)制適配項(xiàng),但是它可以帶給用戶更好的體驗(yàn):

  • 可大幅減少耗電量。OLED 屏幕中每個(gè)像素都是自主發(fā)光,所以在顯示深色元素時(shí)像素所消耗的電流更低,尤其在純黑顏色時(shí)像素點(diǎn)可以完全關(guān)閉來(lái)達(dá)到省電的效果。

  • 為弱視以及對(duì)強(qiáng)光敏感的用戶提高可視性。深色可以降低屏幕的整體視覺(jué)亮度,減少對(duì)眼睛的視覺(jué)壓力。

  • 讓所有人都可以在光線較暗的環(huán)境中更輕松地使用設(shè)備。

適配方法有兩種:

1、手動(dòng)適配(資源替換)

官方文檔中提到的繼承Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight的方法,但這只是將我們使用的各種View的默認(rèn)樣式進(jìn)行了適配,并不太適用于實(shí)際項(xiàng)目的適配。因?yàn)榫唧w的項(xiàng)目中的View都按照設(shè)計(jì)的風(fēng)格進(jìn)行了重定義。

其實(shí)適配的方法很簡(jiǎn)單,類(lèi)似屏幕適配、國(guó)際化的操作,并不需要繼承上面的主題。比如你要修改顏色,就在res 下新建 values-night目錄,創(chuàng)建對(duì)應(yīng)的colors.xml文件。將具體要修改的色值定義在里面。圖標(biāo)之類(lèi)的也是一個(gè)思路,創(chuàng)建對(duì)應(yīng)的 drawable-night目錄。

只要你之前的代碼不是硬編碼且代碼規(guī)范,那么適配起來(lái)還是很輕松。

2、自動(dòng)適配(Force Dark)

Android 10 提供 Force Dark 功能。一如其名,此功能可讓開(kāi)發(fā)者快速實(shí)現(xiàn)深色主題背景,而無(wú)需明確設(shè)置 DayNight 主題背景。

如果您的應(yīng)用采用淺色主題背景,則 Force Dark 會(huì)分析應(yīng)用的每個(gè)視圖,并在相應(yīng)視圖在屏幕上顯示之前,自動(dòng)應(yīng)用深色主題背景。有些開(kāi)發(fā)者會(huì)混合使用 Force Dark 和本機(jī)實(shí)現(xiàn),以縮短實(shí)現(xiàn)深色主題背景所需的時(shí)間。

應(yīng)用必須選擇啟用 Force Dark,方法是在其主題背景中設(shè)置 android:forceDarkAllowed="true"。

此屬性會(huì)在所有系統(tǒng)及 AndroidX 提供的淺色主題背景(例如 Theme.Material.Light)上設(shè)置。使用 Force Dark 時(shí),您應(yīng)確保全面測(cè)試應(yīng)用,并根據(jù)需要排除視圖。

如果您的應(yīng)用使用Dark Theme主題(例如Theme.Material),則系統(tǒng)不會(huì)應(yīng)用 Force Dark。同樣,如果應(yīng)用的主題背景繼承自 DayNight 主題(例如Theme.AppCompat.DayNight),則系統(tǒng)不會(huì)應(yīng)用 Force Dark,因?yàn)闀?huì)自動(dòng)切換主題背景。

您可以通過(guò) android:forceDarkAllowed 布局屬性或 setForceDarkAllowed(boolean) 在特定視圖上控制 Force Dark。

上述內(nèi)容我直接照搬文檔的說(shuō)明。

總結(jié)一下,使用Force Dark需要注意幾點(diǎn):

  1. 如果使用的是 DayNight 或 Dark Theme 主題,則設(shè)置forceDarkAllowed 不生效。

  2. 如果有需要排除適配的部分,可以在對(duì)應(yīng)的View上設(shè)置forceDarkAllowed為false。

這里說(shuō)說(shuō)我實(shí)際使用此方法的感受:整體還是不錯(cuò)的,設(shè)置的色值會(huì)自動(dòng)取反。但也因此顏色不受控制,能否達(dá)到預(yù)期效果是個(gè)需要注意的問(wèn)題。追求快速適配可以采取此方案。

手動(dòng)切換主題

使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)方法,其中參數(shù)mode有以下幾種:

  • 淺色 - MODE_NIGHT_NO
  • 深色 - MODE_NIGHT_YES
  • 由省電模式設(shè)置 - MODE_NIGHT_AUTO_BATTERY
  • 系統(tǒng)默認(rèn) - MODE_NIGHT_FOLLOW_SYSTEM

下面的代碼是官方Demo中的使用示例:

public class ThemeHelper {

    public static final String LIGHT_MODE = "light";
    public static final String DARK_MODE = "dark";
    public static final String DEFAULT_MODE = "default";

    public static void applyTheme(@NonNull String themePref) {
        switch (themePref) {
            case LIGHT_MODE: {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                break;
            }
            case DARK_MODE: {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                break;
            }
            default: {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
                } else {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
                }
                break;
            }
        }
    }
}

通過(guò)AppCompatDelegate.getDefaultNightMode()方法,可以獲取到當(dāng)前的模式,這樣便于代碼中去適配。

監(jiān)聽(tīng)深色主題是否開(kāi)啟

首先在清單文件中給對(duì)應(yīng)的Activity配置 android:configChanges="uiMode":

<activity
    android:name=".MyActivity"
    android:configChanges="uiMode" />

這樣在onConfigurationChanged方法中就可以獲?。?/p>

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
    switch (currentNightMode) {
        case Configuration.UI_MODE_NIGHT_NO:
            // 關(guān)閉
            break;
        case Configuration.UI_MODE_NIGHT_YES:
            // 開(kāi)啟
            break;
        default:
            break;    
    }
}

詳細(xì)的內(nèi)容你可以參看官方文檔和官方Demo。

https://developer.android.google.cn/guide/topics/ui/look-and-feel/darktheme

https://github.com/android/user-interface-samples/tree/master/DarkTheme

判斷深色主題是否開(kāi)啟

其實(shí)和上面onConfigurationChanged方法同理:

public static boolean isNightMode(Context context) {
    int currentNightMode = context.getResources().getConfiguration().uiMode & 
        Configuration.UI_MODE_NIGHT_MASK;
    return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
}

5、標(biāo)識(shí)符和數(shù)據(jù)

對(duì)不可重置的設(shè)備標(biāo)識(shí)符實(shí)施了限制

受影響的方法包括:

Build

  • getSerial()

TelephonyManager

  • getImei()
  • getDeviceId()
  • getMeid()
  • getSimSerialNumber()
  • getSubscriberId()

從 Android 10 開(kāi)始,應(yīng)用必須具有 READ_PRIVILEGED_PHONE_STATE 特許權(quán)限才能正常使用以上這些方法。

如果你的應(yīng)用沒(méi)有該權(quán)限,卻仍然使用了以上的方法,則返回的結(jié)果會(huì)因目標(biāo) SDK 版本而異:

  • 如果應(yīng)用以** Android 10 或更高版本為目標(biāo)平臺(tái)**,則會(huì)發(fā)生 SecurityException。
  • 如果應(yīng)用以 Android 9(API 級(jí)別 28)或更低版本為目標(biāo)平臺(tái),則相應(yīng)方法會(huì)返回 null 或占位符數(shù)據(jù)(如果應(yīng)用具有 READ_PHONE_STATE 權(quán)限)。否則,會(huì)發(fā)生 SecurityException。

這項(xiàng)改動(dòng)表示第三方應(yīng)用無(wú)法獲取Device ID這類(lèi)唯一標(biāo)識(shí)。如果你需要唯一標(biāo)識(shí)符,請(qǐng)參閱文檔:唯一標(biāo)識(shí)符的最佳做法。

https://developer.android.google.cn/training/articles/user-data-ids

當(dāng)然你也可以試試移動(dòng)安全聯(lián)盟(MSA)聯(lián)合多家廠商共同開(kāi)發(fā)的統(tǒng)一補(bǔ)充設(shè)備標(biāo)識(shí)調(diào)用SDK。據(jù)說(shuō)還有點(diǎn)不穩(wěn)定,因?yàn)槲視簳r(shí)還沒(méi)有嘗試過(guò),所以不做評(píng)價(jià)。

http://msalliance.icoc.bz/col.jsp?id=120

限制了對(duì)剪貼板數(shù)據(jù)的訪問(wèn)權(quán)限

除非您的應(yīng)用是默認(rèn)輸入法 (IME) 或是目前處于焦點(diǎn)的應(yīng)用,否則它無(wú)法訪問(wèn) Android 10 或更高版本平臺(tái)上的剪貼板數(shù)據(jù)。

對(duì)啟用和停用 WLAN 實(shí)施了限制

以 Android 10 或更高版本為目標(biāo)平臺(tái)的應(yīng)用無(wú)法啟用或停用 WLAN。WifiManager.setWifiEnabled()方法始終返回 false。

如果您需要提示用戶啟用或停用 WLAN,請(qǐng)使用設(shè)置面板。

https://developer.android.google.cn/about/versions/10/features#settings-panels

6、其他

Android10上對(duì)折疊屏設(shè)備有了更好的支持,對(duì)于有折疊屏適配的需求,可以參看為可折疊設(shè)備構(gòu)建應(yīng)用 和 華為折疊屏應(yīng)用開(kāi)發(fā)指導(dǎo)。

https://developer.android.google.cn/guide/topics/ui/foldables

https://developer.huawei.com/consumer/cn/doc/90101

以上內(nèi)容只是Android 10中比較大的幾項(xiàng)變化,完整的內(nèi)容可以查看官方文檔。

https://developer.android.google.cn/about/versions/10/behavior-changes-all

最后,點(diǎn)個(gè)贊關(guān)注鼓勵(lì)一下~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容