Android 14 應(yīng)用適配指南

Android 14 應(yīng)用適配指南:https://dev.mi.com/distribute/doc/details?pId=1718

1.獲取Android 14

1.1 谷歌發(fā)布時間表

https://developer.android.com/about/versions/14/overview#timeline

1.2 小米手機升級Android 14

現(xiàn)在Xiaomi 13、Xiaomi 13 Pro、Xiaomi Pad 6 可通過鏈接,線刷基于Android? 14 Beta 1的MIUI 14開發(fā)者預(yù)覽版。
https://web.vip.miui.com/page/info/mio/mio/detail?postId=40403123&fromPathname=mioSingleBoard&app_version=dev.230112
如您暫未擁有上述設(shè)備也沒關(guān)系,我們?yōu)槟峁┝俗懔康脑茰y設(shè)備,來支持廣大開發(fā)者的適配工作,云測平臺詳見:https://testit.miui.com/android_14 (云測平臺使用權(quán)限申請:https://m.beehive.miui.com/Ooq42P35TPAIP-sZJT1EqA

1.3 Google原生機升級Android 14

開發(fā)者持有Pixel系列的機器可以直接ota升級,或者下載鏡像升級,具體見鏈接:
https://developer.android.google.cn/about/versions/14/download

1.4 Android模擬器

在Android Studio中,可按照如下方式安裝Android 14 SDK:

  • 依次點擊Tools>SDK Manager。
  • 在“SDK Plateforms”標(biāo)簽頁中,選擇Android 14。
  • 在“SDK Tools”標(biāo)簽頁中,選擇Android SDK Build-Tools 34。

點擊OK,安裝SDK。

如需訪問 Android 14 API 并測試您的應(yīng)用與 Android 14 的兼容性,請打開模塊級build.gradle或build.gradle.kts文件,并使用 Android 14所對應(yīng)的值對它們進行更新:如何設(shè)置這些值的格式取決于您所使用的 Android Gradle 插件 (AGP) 版本。

注意:如果您尚未準(zhǔn)備好完全支持 Android 14,您仍然可以使用可調(diào)試的應(yīng)用、Android 14 設(shè)備和兼容性框架來執(zhí)行應(yīng)用兼容性測試,而無需更改應(yīng)用以使其與預(yù)覽版 SDK 兼容或以此為目標(biāo)平臺。

2.新功能和API

2.1 應(yīng)用級語言偏好

Android 14 擴展了 Android 13(API level 33)中引入的App級語言功能:

  1. 自動生成App的 localeConfig:從 Android Studio Giraffe Canary 7 和 AGP 8.1.0-alpha07 開始,開發(fā)者可以配置應(yīng)用以自動支持每個應(yīng)用的語言偏好。根據(jù)項目資源,Android Gradle 插件生成 LocaleConfig 文件并在最終AndroidManifest.xml中添加對它的引用,因此不再需要手動創(chuàng)建或更新該文件。 AGP 使用App模塊的 res 文件夾中的資源和庫模塊依賴項來確定要包含在 LocaleConfig 文件中的區(qū)域設(shè)置。
  2. 應(yīng)用的 localeConfig 的動態(tài)更新:使用 LocaleManager 中的 setOverrideLocaleConfig() 和 getOverrideLocaleConfig() 在設(shè)備的系統(tǒng)設(shè)置中動態(tài)更新App的支持語言列表。使用這個特性來自定義每個區(qū)域支持的語言列表、做 A/B 測試,或者如果App利用服務(wù)器端推送進行本地化,則提供更新的語言環(huán)境列表。

我們在此展示一個動態(tài)修改系統(tǒng)設(shè)置中可選語言列表的例子:
我們名為multilingual settings的demo App中res/xml/locales_config.xml內(nèi)容如下:

<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="en"/>
    <locale android:name="en-GB"/>
    <locale android:name="fr"/>
    <locale android:name="ja"/>
    <locale android:name="zh-Hans-MO"/>
    <locale android:name="zh-Hant-MO"/>
</locale-config>復(fù)制

然后在manifest.xml的application下加入:

android:localeConfig="@xml/locales_config"
復(fù)制

打開設(shè)置 > 系統(tǒng) > 語言和輸入法 > 應(yīng)用語言 > multilingual settings,可以看到我們在locals_config.xml中編寫的可選語言列表:

若我們在App內(nèi)通過按鈕調(diào)用U上新增的setOverrideLocaleConfig方法:
MainActivity#overrideLocalConfig:

public void overrideLocalConfig(View view) {
    this.getSystemService(LocaleManager.class).setOverrideLocaleConfig(new LocaleConfig(new LocaleList(Locale.ENGLISH, Locale.GERMAN, Locale.KOREAN, Locale.ITALIAN)));
}復(fù)制

按下按鈕后,我們回到設(shè)置 > 系統(tǒng) > 語言和輸入法 > 應(yīng)用語言 > multilingual settings,發(fā)現(xiàn):

系統(tǒng)設(shè)置中的可選語言列表變成了我們在setOverrideLocaleConfig中的參數(shù)所代表的語言。
而在T上,我們沒有overrideLocalConfig API,系統(tǒng)設(shè)置中的App的可選語言列表只能由res/xml/locales_config.xml指定,一旦App編譯完成便無法更改。overrideLocalConfig給了我們動態(tài)更改的能力。

3.輸入法 (IME) 的App語言可見性:IME 可以利用 getApplicationLocales() 方法檢查當(dāng)前App的語言并將 IME 語言與該語言相匹配。

2.2 語法性別變化API

30 億人使用帶有性別的語言:語法類別(例如名詞、動詞、形容詞和介詞)根據(jù)交談或談?wù)摰娜撕蛯ο蟮男詣e而變化的語言。傳統(tǒng)上,許多性別語言使用陽性語法性別作為默認(rèn)或通用性別。
以錯誤的語法性別稱呼用戶,例如以男性語法性別稱呼女性,會對他們的表現(xiàn)和態(tài)度產(chǎn)生負面影響。相比之下,具有正確反映用戶語法性別的語言的 UI 可以提高用戶參與度并提供更加個性化和自然的用戶體驗。
為了幫助開發(fā)者為性別語言構(gòu)建以用戶為中心的 UI,Android 14 引入了語法性別變形 API,從而無需重構(gòu)應(yīng)用即可添加對語法性別的支持。
本特性的調(diào)用較為簡單,首先在Android Studio中,在res文件夾下新建命名如values-fr-feminine的文件夾,在里面新建strings.xml,并僅將涉及語法性別的文本包含進去。本例中,不涉及語法性別的法語文本應(yīng)被放進res/values-fr/strings.xml中。官方文檔可見為帶有語法性別的語言添加翻譯。
然后,我們即可為應(yīng)用設(shè)定語法性別:


this.getSystemService(GrammaticalInflectionManager.class).setRequestedApplicationGrammaticalGender(Configuration.GRAMMATICAL_GENDER_FEMININE);復(fù)制

上面的代碼將應(yīng)用的語法性別設(shè)為陰性,即假定用戶為女性。
要獲取App的語法性別int值,可以:


int gender = this.getSystemService(GrammaticalInflectionManager.class).getApplicationGrammaticalGender();復(fù)制

Android 14上共有4種與語法性別相關(guān)的int值,分別為:


Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED, Configuration.GRAMMATICAL_GENDER_NEUTRAL, Configuration.GRAMMATICAL_GENDER_FEMININE, Configuration.GRAMMATICAL_GENDER_MASCULINE復(fù)制

2.3 地區(qū)偏好

區(qū)域偏好使用戶能夠個性化溫度單位、一周的第一天和編號系統(tǒng)。居住在美國的歐洲人可能更喜歡以攝氏度而不是華氏度為單位的溫度單位,并且希望應(yīng)用將星期一視為一周的開始,而不是美國默認(rèn)的星期日。
Android 14為用戶提供了一個集中的位置來更改這些應(yīng)用偏好。這些偏好也會在備份和恢復(fù)過程中持續(xù)存在。多個 API 和intent(例如 getTemperatureUnit 和 getFirstDayOfWeek)授予應(yīng)用讀取用戶偏好的權(quán)限,因此應(yīng)用可以調(diào)整其顯示信息的方式。開發(fā)者還可以在 ACTION_LOCALE_CHANGED 上注冊 BroadcastReceiver 以在區(qū)域偏好更改時收到廣播。
要找到這些設(shè)置,請打開“Settings”并導(dǎo)航至System > Languages & input > Regional preferences。

2.4 分享表自定義行為和改善直接共享目標(biāo)的排名

Android 14更新了系統(tǒng)的分項表,可支持自定義app行為,并且為用戶提供更多有用的預(yù)覽信息。

2.4.1 添加自定義的行為

在Android 14,app可以在系統(tǒng)分享表中自定義行為。在分享表中,可借助ChooserAction.Builder來構(gòu)建自定義ChooserAction,指定ChooserActions的列表作為使用Intent.createChooser創(chuàng)建的Intent的Intent.EXTRA_CHOOSER_CCUSTOM_ACTIONS。

以下是創(chuàng)建自定義行為的一般過程

以發(fā)送多張圖片為例
//創(chuàng)建Intent
Intent shareIntent1 =new Intent();
shareIntent1.setAction(Intent.ACTION_SEND_MULTIPLE);
shareIntent1.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
shareIntent1.setType("image/*");//各種類型的圖像

Intent shareIntent = Intent.createChooser(shareIntent1,null);

shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

//添加自定義行為的intent
PendingIntent customAction = PendingIntent.getBroadcast(
        mContext,
        1,
        new Intent(CHOOSER_CUSTOM_ACTION_BROADCAST_ACTION),
        PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent fakeCustomAction = PendingIntent.getBroadcast(
        mContext,
        1,
        new Intent("some_action"),
        PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT);

PendingIntent fakeCustomAction2 = PendingIntent.getActivity(
        mContext,
        1,
        new Intent(CHOOSER_CUSTOM_ACTION_BROADCAST_ACTION),
        PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT);
//創(chuàng)建action
ChooserAction[] actions = new ChooserAction[] {
        createChooserAction("act1", fakeCustomAction2),
        createChooserAction("act2", fakeCustomAction),
        createChooserAction("act3", fakeCustomAction),
        createChooserAction("act4", fakeCustomAction),
        createChooserAction("act5", customAction),
};
PendingIntent modifyShare = PendingIntent.getBroadcast(
        mContext,
        1,
        new Intent(CHOOSER_CUSTOM_ACTION_BROADCAST_ACTION),
        PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT);
//創(chuàng)建修改分享媒體資源的action
ChooserAction modifyShareAction = new ChooserAction.Builder(
        Icon.createWithResource(
                mContext.getPackageName(),
                R.drawable.img),
        modifyShareLabel,
        modifyShare
).build();
//將自定義行為存到intent里
shareIntent.putExtra(Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, modifyShareAction);
mContext.startActivity(shareIntent);復(fù)制

2.4.2 改善直接共享目標(biāo)的排名

Android 14中為了給用戶提供更有用的結(jié)果,其使用更多來自app的標(biāo)簽來決定直接分享對象的排名。為了提供更有用的標(biāo)簽,當(dāng)用戶向聯(lián)系人發(fā)送消息時,通過使用相應(yīng)的快捷方式調(diào)用pushDynamicShortcut來報告快捷方式的使用情況。并通過調(diào)用ShortcutInfoCompat.Builder#addCapabilityBinding(“actions.intent.SEND_MESSAGE”)將相應(yīng)的功能“actions.intent.SEND.MESSAGE”附加到該快捷方式。

以下方法可有助于提高直接共享目標(biāo)的排名

  1. 確保所有 shortcutId 獨一無二,且不在不同的目標(biāo)上重復(fù)使用。
  2. 通過調(diào)用 setLongLived(true) 確??旖莘绞介L期存在。
  3. 直接提供排名,借助setRank()來設(shè)置
  4. 對于社交應(yīng)用
  • 在快捷方式中為相關(guān)的Person對象提供set鍵
  • 快捷方式連接到來自同一個人或多個人的相關(guān)通知
  • 在提供的Person對象中,提供設(shè)備上的關(guān)聯(lián)聯(lián)系人的有效URI

2.5 支持應(yīng)用內(nèi)部動畫和自定義動畫

Android 13在開發(fā)者選項中引入了預(yù)測性返回動畫。當(dāng)在開發(fā)者選項中開啟受支持的應(yīng)用使用時,向后滑動會顯示一個動畫,指示返回手勢退出應(yīng)用程序并返回主屏幕。

Android 14中對預(yù)測性返回進行了多項改進并添加多項新的功能:

在新發(fā)布的Android 14預(yù)覽版中,預(yù)測性返回所有的功能仍然保留在開發(fā)者選項中。請參閱開發(fā)者者指南進行支持應(yīng)用的預(yù)測性返回手勢,同樣借助開發(fā)者指南進行創(chuàng)建自定義應(yīng)用內(nèi)部過渡動畫。

2.6 對應(yīng)用商店的改善

Android 14引入了幾個新的PackageInstaller API,以改善應(yīng)用商店的用戶體驗。

2.6.1 在下載前請求用戶同意

一般情況下,安裝或更新應(yīng)用需要得到用戶的同意。當(dāng)安裝器利用REQUEST_INSTALL_PACKAGES權(quán)限嘗試去安裝一個新的應(yīng)用,在Android 14以前的版本中,應(yīng)用可以在APKs寫入到session,且已提交session的后面向用戶申請權(quán)限。
從Android 14開始,借助requestUserPreapproval()方法可以讓安裝器在提交session前申請用戶的同意。這一改善讓應(yīng)用商店征得用戶安裝同意后開始下載APKs。并且一旦用戶同意安裝后,應(yīng)用商店就可在后臺進行下載和安裝,這期間不會干擾用戶的正常使用,有利于改善用戶體驗。

2.6.2 負責(zé)未來更新

安裝者借助新的方法setRequestUpdateOwnership()向系統(tǒng)表明它要對未來它安裝的應(yīng)用的更新負責(zé)。此功能強制執(zhí)行了更新所有權(quán),意味著只有更新所有者才能安裝應(yīng)用程序的自動更新。強制認(rèn)定更新所有者有助于確保用戶僅從預(yù)期的應(yīng)用商店接收更新。
任何其他的安裝者,包括使用INSTALL_PACKAGES權(quán)限的,都必須明確收到用戶的同意才能安裝更新。如果用戶決定從其他地方更新應(yīng)用,擁有更新所有權(quán)的安裝者將失去更新所有權(quán)。

2.6.3 盡量以較少的中斷時間來更新應(yīng)用

應(yīng)用商店應(yīng)該避免去更新用戶正在使用的應(yīng)用,這是因為更新操作可能會殺死正在使用的應(yīng)用,會導(dǎo)致中斷用戶正在做的事情。
從Android 14開始,IntallConstraints API提供給用戶一個方式確保應(yīng)用在一個合適的時間進行更新。例如,應(yīng)用商店可通過調(diào)用commitSessionAfterInstallConstraintsAreMet()方法來確保提交更新時用戶不再與應(yīng)用交互。

2.6.4 無縫安裝拆分的APK

通過拆分,可將一整個APK按功能拆分成幾個分散的APK文件。拆分APKs可讓應(yīng)用商店優(yōu)化應(yīng)用不同組件的發(fā)布。例如,應(yīng)用商店可能基于目標(biāo)硬件的特性來進行優(yōu)化。自API 22,PackageInstaller API開始支持拆分APK操作。
在Android 14中,安裝者借助setDontKillApp()方法聲明當(dāng)安裝新的被拆分APK的部分組成時,不應(yīng)該殺死正在運行應(yīng)用的進程。當(dāng)用戶正在使用應(yīng)用時,應(yīng)用商店可以借助這個功能來無縫安裝應(yīng)用新的部分內(nèi)容。

2.7 監(jiān)測用戶截屏行為

為了創(chuàng)建標(biāo)準(zhǔn)化的屏幕截圖檢測體驗,Android 14引入了一個隱私保護的屏幕截屏檢測API。這個API允許應(yīng)用程序在每一個活動的基礎(chǔ)上進行注冊回調(diào)。當(dāng)Activity在可見的情況下進行截屏?xí)r,Activity會調(diào)用這些回調(diào),并通知用戶一些信息。

API使用流程

  • 聲明"android.permission.DETECT_SCREEN_CAPTURE"權(quán)限
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />復(fù)制
  • 實現(xiàn)一個回調(diào)接口,重寫onScreenCapture()函數(shù)。
final Activity.ScreenCaptureCallback screenCaptureCallback =
    new Activity.ScreenCaptureCallback() {
        @Override
        public void onScreenCaptured() {
            // Add logic to take action in your app.
        }
    };復(fù)制
  • 注冊:在Activity的onStart()方法中注冊screenCaptureCallback()
@Override
protected void onStart() {
    super.onStart();
    // Pass in the callback created in the previous step
    // and the intended callback executor (e.g. Activity's mainExecutor).
    registerScreenCaptureCallback(executor, screenCaptureCallback);
}復(fù)制
  • 取消注冊:在activity的onStop()方法取消注冊screenCaptureCallback
@Override
protected void onStop() {
    super.onStop();
    unregisterScreenCaptureCallback(screenCaptureCallback);
}復(fù)制

注意:截屏方式觸發(fā)回調(diào)函數(shù)執(zhí)行的條件:

  1. 生效:只有當(dāng)用戶執(zhí)行特定的按鍵組合才會觸發(fā),比如電源鍵+音量鍵下鍵
  2. 不生效:通過adb或者捕獲設(shè)備當(dāng)前屏幕內(nèi)容的儀器測試中拍攝的屏幕快照,不會調(diào)用回調(diào)函數(shù)。

2.8 路徑是可查詢和可插值的

Android的Path API是一種強大而靈活的機制,用于創(chuàng)建和渲染矢量圖形,能夠繪制或填充路徑,并且通過構(gòu)建線段或者二次曲線或者三次曲線路徑,然后執(zhí)行相應(yīng)的運算來獲得更復(fù)雜的形狀。其中路徑對象的內(nèi)部信息對調(diào)用方是不透明的。
創(chuàng)建一個路徑,可通過方法moveTo()、lineTo()和cubicTo()等方法添加路徑片段。但是對整個路內(nèi)部的每一段內(nèi)容無法訪問,因此你必須要在創(chuàng)建路徑開始的時候保存相關(guān)信息。

從Android 14開始,你可以查詢每段路徑的內(nèi)容。路徑內(nèi)容都保存在PathIterator對象里面,該對象可通過Path.getPathIterator方法獲得。

2.8.1 查詢路徑片段內(nèi)容

Path path = new Path();
path.moveTo(1.0F, 1.0F);
path.lineTo(2.0F, 2.0F);
path.close();
PathIterator pathIterator = path.getPathIterator();復(fù)制

通過以上代碼可獲取PathIterator的對象,該對象包含了路徑片段的信息。通過PathIterator的迭代器可訪問PathIterator的對象的路徑信息。

while (pathIterator.hasNext()) {
    PathIterator.Segment segment = pathIterator.next();
    Log.i(LOG_TAG, "segment: " + segment.getVerb() + ", " + segment.getPoints());
}復(fù)制

2.8.2 路徑插值

通過查詢路徑可用來對路徑進行插值。即通過兩條內(nèi)部結(jié)構(gòu)相同的路徑,通過interpolate()插值的方法來生成一條新的路徑。通過兩條構(gòu)建兩條路徑path和path2,借助插值方法interpolate()得到resultPath,resultPath是由path和path2線性插值得到,插值因子是0.8f。三條路徑如下圖所示。

Path path = new Path();
path.lineTo(300, 1200);
Path path2 = new Path();
path2.moveTo(300f,600f);
path2.lineTo(600, 1200);
Path resultPath = new Path();
if(path.isInterpolatable(path2)){
    path.interpolate( path2, 0.8f, resultPath);
}復(fù)制

注意:兩條路徑能夠進行插值,需要兩條路徑的內(nèi)部結(jié)構(gòu)相同。即路徑中點的數(shù)量相同;對應(yīng)路徑片段構(gòu)建的方法相同,比如都是用lineTo()方法構(gòu)建;以及曲線權(quán)重相同(目前使用權(quán)重的方法是conicTo(float x1, float y1, float x2, float y2, float weight))。

2.9 OpenJDK 17 更新

  • 更新了大約 300 個 java.base 類以支持 Java 17。
  • Text Blocks,將多行字符串文字引入Java。
  • instanceof的模式匹配,它允許將對象視為具有instanceof中的特定類型,而無需任何其他變量。
  • 密封類,允許限制哪些類和接口可以擴展或?qū)崿F(xiàn)它們。

3.影響應(yīng)用的行為變更

3.1行為變更:所有應(yīng)用

3.1.1 設(shè)置精確鬧鐘權(quán)限默認(rèn)關(guān)閉

精確鬧鐘適用于需要在精確時間發(fā)生的用戶意圖的通知或操作。
SCHEDULE_EXACT_ALARM 是 Android 12 中引入的允許應(yīng)用安排準(zhǔn)確鬧鐘的權(quán)限,該權(quán)限不再預(yù)先授予大多數(shù)targetSDK>= Android 14的新安裝App(默認(rèn)設(shè)置為拒絕)。如果用戶通過備份還原操作將應(yīng)用數(shù)據(jù)傳輸?shù)竭\行Android 14的設(shè)備上,權(quán)限仍然會被拒絕。如果現(xiàn)有的應(yīng)用已經(jīng)擁有此權(quán)限,它將在設(shè)備升級到 Android 14 時預(yù)先授予。
需要 SCHEDULE_EXACT_ALARM 權(quán)限才能通過以下 API 啟動準(zhǔn)確的警報,否則將拋出 SecurityException:

  • setExact()
  • setExactAndAllowWhileIdle()
  • setAlarmClock()

SCHEDULE_EXACT_ALARM 權(quán)限的現(xiàn)有最佳實踐仍然適用,包括以下內(nèi)容:

  • 在安排確切的警報之前,使用 canScheduleExactAlarms() 檢查權(quán)限。
  • 將應(yīng)用設(shè)置為偵聽前臺廣播 AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED 并對其做出正確反應(yīng),系統(tǒng)會在用戶授予權(quán)限時發(fā)送該廣播。

在U上,App即使在AndroidManifest.xml里申明了


<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />復(fù)制

在安裝后系統(tǒng)也不會自動授予android.permission.SCHEDULE_EXACT_ALARM權(quán)限。若App沒有該權(quán)限,在調(diào)用AlarmManager#setExact,AlarmManager#setAlarmClock和AlarmManager#setExactAndAllowWhileIdle三個設(shè)置精確鬧鐘的方法時,系統(tǒng)會拋出 SecurityException 。
若App想要獲取該權(quán)限,需要用戶到應(yīng)用信息中或Settings->Apps->Special app access->Alarms & reminders中手動打開:

3.1.1.1 受影響的應(yīng)用

如果設(shè)備運行的是 Android 14 或更高版本,此更改將影響新安裝的具有以下特征的應(yīng)用:

  • targetSDK >= Android 13 。
  • 在AndroidManifest.xml中聲明 SCHEDULE_EXACT_ALARM 權(quán)限。
  • 不屬于豁免或授權(quán)前情形。
  • 不是日歷或鬧鐘應(yīng)用。
3.1.1.2 日歷和鬧鐘應(yīng)用應(yīng)該申明USE_EXACT_ALARM權(quán)限

日歷或鬧鐘應(yīng)用需要在應(yīng)用不再運行時發(fā)送日歷提醒、喚醒警報或警報。這些應(yīng)用可以請求 USE_EXACT_ALARM 普通權(quán)限。 USE_EXACT_ALARM 權(quán)限將在安裝時授予,擁有此權(quán)限的應(yīng)用程序?qū)⒛軌蛳窬哂?SCHEDULE_EXACT_ALARM 權(quán)限的應(yīng)用程序一樣安排確切的警報。小米應(yīng)用商店會加強對此權(quán)限的管控。

3.1.1.3 可能不需要精確鬧鐘的使用場景

由于 SCHEDULE_EXACT_ALARM 權(quán)限現(xiàn)在默認(rèn)被拒絕,并且權(quán)限授予過程需要用戶執(zhí)行額外的步驟,因此強烈建議開發(fā)人員評估他們的用例并確定確切的警報是否仍然對他們的用例有意義。
以下列表顯示了可能不需要確切警報的常見工作流程:

  • 在應(yīng)用的生命周期內(nèi)安排重復(fù)工作

如果任務(wù)需要牢記實時約束,例如明天下午 2:00 或 30 分鐘后出發(fā),則 set() 方法很有用。否則,建議改用 postAtTime() 或 postDelayed() 方法。

  • 預(yù)定的后臺工作,例如更新您的應(yīng)用程序和上傳日志

WorkManager 提供了一種方法來安排對時間敏感的周期性工作。您可以提供重復(fù)間隔和 flexInterval(至少 15 分鐘)來定義工作的細粒度運行時間。

  • 當(dāng)系統(tǒng)處于空閑狀態(tài)時,需要在大約某個時間發(fā)出警報

使用不準(zhǔn)確的警報。具體來說,調(diào)用 setAndAllowWhileIdle()。

  • 應(yīng)在特定時間后發(fā)生的用戶指定操作

使用不準(zhǔn)確的警報。具體來說,調(diào)用 set()。

  • 可以在一個時間窗口內(nèi)發(fā)生的用戶指定的操作

使用不準(zhǔn)確的警報。具體來說,調(diào)用 setWindow()。請注意,允許的最小窗口長度為 10 分鐘。

3.1.1.4 繼續(xù)使用精確鬧鐘的遷移步驟

應(yīng)用程序必須在安排確切的警報之前檢查它們是否具有權(quán)限。如果應(yīng)用程序沒有權(quán)限,則它們必須通過調(diào)用意圖向用戶請求權(quán)限。
這與請求特殊權(quán)限的標(biāo)準(zhǔn)工作流程相同:

  1. 應(yīng)用程序應(yīng)調(diào)用 AlarmManager.canScheduleExactAlarms() 以確認(rèn)它具有適當(dāng)?shù)臋?quán)限。
  2. 如果應(yīng)用程序沒有權(quán)限,調(diào)用包含 ACTION_REQUEST_SCHEDULE_EXACT_ALARM 以及應(yīng)用程序包名稱的意圖,以請求用戶授予權(quán)限。在您應(yīng)用的 onResume() 方法中檢查用戶的決定。
  3. 偵聽在用戶授予權(quán)限時發(fā)送的 AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED 廣播。
  4. 如果用戶授予您的應(yīng)用程序權(quán)限,您的應(yīng)用程序可以設(shè)置確切的警報。相反,如果用戶拒絕了該權(quán)限,請優(yōu)雅地降低您的應(yīng)用程序體驗,以便它在沒有受該權(quán)限保護的信息的情況下向用戶提供功能。

最佳實踐:使用alarmManager.canScheduleExactAlarms()首先檢查有無SCHEDULE_EXACT_ALARM權(quán)限,若無,則使用startActivity向用戶申請權(quán)限:

boolean canScheduleExactAlarms = mAlarmManager.canScheduleExactAlarms();
if(!canScheduleExactAlarms) {
    Intent permissionIntent = new Intent();
    permissionIntent.setAction("android.settings.REQUEST_SCHEDULE_EXACT_ALARM");
    startActivity(permissionIntent);
}
復(fù)制
3.1.1.5 在拒絕權(quán)限時適當(dāng)?shù)亟导?/h5>

一些用戶會拒絕授予權(quán)限。在這種情況下,我們建議應(yīng)用程序優(yōu)雅地降低體驗,并仍然通過識別其用例來努力提供最佳的回退用戶體驗。

3.1.1.6 例外

以下類型的應(yīng)用始終允許調(diào)用 setExact() 或 setExactAndAllowWhileIdle() 方法:

  • 使用平臺證書簽名的應(yīng)用程序。
  • 特權(quán)應(yīng)用程序。
  • 電源許可名單上的應(yīng)用程序(如果您的應(yīng)用程序符合要求,您可以使用 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent 操作請求)。
3.1.1.7 預(yù)授權(quán)

SYSTEM_WELLBEING 的角色持有者將被預(yù)先授予 SCHEDULE_EXACT_ALARM。

3.1.1.8 測試指導(dǎo)

要測試此更改,請從系統(tǒng)設(shè)置中的特殊應(yīng)用訪問頁面(設(shè)置 > 應(yīng)用 > 特殊應(yīng)用訪問 > 鬧鐘和提醒)禁用您的應(yīng)用的鬧鐘和提醒權(quán)限,并觀察您的應(yīng)用的行為。

3.1.2 當(dāng)應(yīng)用處于cached狀態(tài)時,動態(tài)注冊的廣播會入隊

Android 14中,當(dāng)應(yīng)用處于緩存狀態(tài)時,系統(tǒng)可以將上下文注冊的廣播放入到隊列中。這類似于Android 12(API級別31)為異步綁定器事務(wù)引入的排隊行為。在Manifest.xml文件中聲明的廣播不會排隊,并且應(yīng)用程序會從緩存狀態(tài)中刪除以發(fā)送廣播。
當(dāng)應(yīng)用離開cached狀態(tài)時,例如回到前臺時,此時系統(tǒng)會分發(fā)排隊的廣播。并且某些廣播的多個實例會被合成一個。依據(jù)其他條件,例如系統(tǒng)運行狀況,應(yīng)用會從緩存狀態(tài)中刪除,并將之前排隊的廣播進行分發(fā)。

3.1.3 三方App只能殺死自己的后臺進程

從 Android 14 開始,當(dāng)App調(diào)用 killBackgroundProcesses() 時,該 API 只能殺死自己的后臺進程。
如果傳入其他App的包名,該方法對該App的后臺進程沒有影響,Logcat中會出現(xiàn)如下信息:


Invalid packageName: com.example.anotherapp復(fù)制

App不應(yīng)使用 killBackgroundProcesses() API 或以其他方式嘗試影響其他App的進程生命周期,即使是在較舊的Android版本上。Android 旨在將緩存的App保留在后臺,并在系統(tǒng)需要內(nèi)存時自動終止它們。如果應(yīng)用不必要地殺死其他應(yīng)用,它會降低系統(tǒng)性能并增加電池消耗,因為稍后需要完全重啟這些應(yīng)用,這比恢復(fù)現(xiàn)有的緩存應(yīng)用占用的資源要多得多。

3.1.4 安裝應(yīng)用要求的最小targetSdk API級別

從Android 14開始,targetSdkVersion低于23的應(yīng)用無法安裝。要求應(yīng)用滿足這些最低目標(biāo)API級別的要求可以提高用戶的安全性和隱私性。

惡意軟件通常針對較舊級別的API,繞過較新Android版本中引入的安全和隱私保護。例如,一些惡意應(yīng)用使用targetSdkVersion 22,以避免受到Android 6.0 Marshmallow(API級別23)于2015年引入的運行時權(quán)限模型的影響。Android 14這一變化使惡意軟件很難避免安全和隱私方面的改進。較低API級別的應(yīng)用將會安裝失敗,并且在日志中顯示以下消息:

INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 7復(fù)制

在升級到Android 14的設(shè)備上,任何targetSdkVersion低于23的應(yīng)用都會保持安裝狀態(tài)。

如果需要測試針對舊API級別的應(yīng)用程序,請使用以下adb命令:

adb install --bypass-low-target-sdk-block FILENAME.apk復(fù)制

3.1.5 隱藏媒體所在應(yīng)用包名

媒體存儲支持查詢OWNER_PACKAGE_NAME列,該列表示存儲媒體文件的應(yīng)用包名。從Android 14開始要滿足以下兩個條件之一,才可以查詢到該列的值。

  • 存儲媒體文件的應(yīng)用程序的程序包名稱對其他應(yīng)用程序可見

如果想要某程序包名讀其他應(yīng)用可見,那么其他應(yīng)用必須在Manifest.xml文件中添加<queries>,package的包名是想要申請對方app的包名。

例如,testmediastore想要查詢testmediastore2的信息,那么Mainfest.xml文件中<queries>的包名就是testmediastore2的包名。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.mimi.testmediastore">
    //.../
    <queries>
        <package android:name="com.mimi.testmediastore2"/>
    </queries>
    //../
</manifest>復(fù)制
  • 查詢媒體存儲的應(yīng)用申請QUERY_ALL_PACKAGE權(quán)限。

3.1.6 常駐通知的變化

如果應(yīng)用向用戶顯示常駐通知,Android 14 允許用戶關(guān)閉此類通知。
此更改適用于通過 Notification.Builder#setOngoing(true) 或 NotificationCompat.Builder#setOngoing(true) 設(shè)置 Notification.FLAG_ONGOING_EVENT 來阻止用戶手動刪除前臺通知的應(yīng)用。 FLAG_ONGOING_EVENT 的行為已更改為使此類通知實際上可由用戶手動刪除。

在以下情況下,此類通知仍然不可關(guān)閉:

  • 當(dāng)手機被鎖定時
  • 如果用戶選擇清除所有通知操作(這有助于防止意外解雇)
  • 此外,此新行為不適用于以下用例中的不可關(guān)閉通知:
  • 使用 CallStyle 綁定到真實呼叫的通知
  • 使用 MediaStyle 創(chuàng)建的通知
  • 設(shè)備策略控制器 (DPC) 和企業(yè)支持包

3.1.7 授權(quán)訪問部分照片或視頻

當(dāng)Android 14上的應(yīng)用程序請求在Android 13(API級別33)引入的權(quán)限READ_MEDIA_IMAGES 或READ_MEDIA_VIDEO時,可以選擇授權(quán)部分訪問權(quán)限。
注意:targetSDK≥33,Android 14上的應(yīng)用都會受到此更改的影響。

3.1.7.1 新的彈窗顯示內(nèi)容

Android 14之前:

訪問媒體圖片或視頻要申請權(quán)限READ_MEDIA_IMAGES和READ_MEDIA_VIDEO。當(dāng)應(yīng)用申請以上兩個權(quán)限任何一個的時候,彈出的權(quán)限框只有以下兩個選項。即允許(允許是允許所有照片)和不允許。

Android 14:

當(dāng)申請權(quán)限READ_MEDIA_IMAGES和READ_MEDIA_VIDEO訪問媒體文件的時候,其彈框中有三個選項。Select photos and videos: 選擇部分圖片允許應(yīng)用訪問
Allow all: 允許訪問所有圖片
Don't allow: 拒絕所有圖片的訪問

如果用戶選擇"SELECT PHOTOS AND VIDEOS",那么稍后應(yīng)用再次請求READ_MEDIA_IMAGES或READ_MEDID_VIDEO,系統(tǒng)將顯示不同的對話框,讓用戶有機會授予完全訪問權(quán)限、保留當(dāng)前選擇或授予其他照片和視頻。一段時間后,系統(tǒng)會直接顯示系統(tǒng)選擇器。

為了幫助應(yīng)用程序支持新的更改,系統(tǒng)引入了一個新的權(quán)限READ_MEDIA_VISUAL_USER_SELECTED。根據(jù)您的應(yīng)用程序是否使用新權(quán)限,系統(tǒng)的行為會有所不同。

注意:如果您的應(yīng)用程序已經(jīng)使用了照片選擇器,則無需采取任何措施來支持此更改。否則,請考慮使用照片選擇器,而不是采用此更改。

3.1.7.2 如果應(yīng)用沒有適配

如果沒有聲明READ_MEDIA_VISUAL_USER_SELECTED權(quán)限,則會發(fā)生以下現(xiàn)象:READ_MEDIA_IMAGES和READ_MEDI_AVIDEO權(quán)限在應(yīng)用會話期間授予,并且提供臨時權(quán)限授予和對用戶選擇的照片和視頻的臨時訪問。 當(dāng)用戶主動殺死應(yīng)用時,系統(tǒng)最終會拒絕這些權(quán)限。這種行為和其他一次性權(quán)限一樣。
如果應(yīng)用以后要訪問其他照片和視頻,則必須手動再次請求READ_MEDIA_IMAGES或READ_MEDIA_VIDEO權(quán)限。該系統(tǒng)遵循與初始權(quán)限請求相同的流程,提示用戶選擇照片和視頻。

如果應(yīng)用遵循最佳方案,則此更改不會影響應(yīng)用。假設(shè)應(yīng)用不保留URI訪問、不存儲系統(tǒng)權(quán)限狀態(tài)或者在權(quán)限更改后不刷新顯示的圖像集,則情況表現(xiàn)如此。然而,根據(jù)應(yīng)用的不同情況,可能表現(xiàn)不會理想。
注意:你的應(yīng)用不需要任何媒體權(quán)限,就可使用照片選擇器或任何系統(tǒng)意圖,例如ACTION_GET_CONTENT或ACTION_OPEN_DOCUMENT。

3.1.7.3 遷移以控制應(yīng)用中的行為

如果聲明了READ_MEDIA_VISUAL_USER_SELECTED權(quán)限,并且用戶選擇了系統(tǒng)彈框中的"SELECT PHOTOS AND VIDEOS",接下來會出現(xiàn)以下情況:READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO權(quán)限都會被拒絕。
授予應(yīng)用READ_MEDIA_VISUAL_USER_SELECTED權(quán)限,用來臨時訪問用戶已經(jīng)選擇的照片和視頻。
如果想要申請其他的照片或視頻,你必須要再次申請READ_MEDIA_IMAGES 或 READ_MEDIA_VIDEO權(quán)限。

注意:在應(yīng)用中創(chuàng)建一個UI界面,用戶必須在重新申請READ_MEDIA_IMAGES 或 READ_MEDIA_VIDEO權(quán)限前按下該界面。防止用戶再次看到系統(tǒng)彈框而感到驚訝。

READ_MEDIA_IMAGES和READ_MEDI_AVIDEO是用戶用來訪問照片和視頻媒體庫的其他唯一權(quán)限。聲明READ_MEDIA_VISUAL_USER_SELECTED會讓權(quán)限控制器意識到您的應(yīng)用支持手動重新請求選擇更多照片和視頻。

為了防止用戶看到多個系統(tǒng)權(quán)限請求彈框,請在一次操作中申請 READ_MEDIA_VISUAL_USER_SELECTED、ACCESS_MEDIA_LOCATION 和訪問媒體的權(quán)限(READ_MEDIA_IMAGES或READ_MEDIA_VIDEO或兩個一起)。

3.1.7.4 設(shè)備升級時保留訪問照片和視頻的權(quán)限

如果應(yīng)用所在設(shè)備從以前的安卓版本升級到Android 14版本,系統(tǒng)會保持對用戶照片和視頻的完全訪問權(quán)限,并自動授予應(yīng)用一些權(quán)限。最終授予的權(quán)限取決于設(shè)備升級到Android 14之前應(yīng)用所擁有的權(quán)限。

  • 從Android 13升級
  1. 應(yīng)用安裝在Android 13。
  2. 應(yīng)用擁有READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO權(quán)限。
  3. 當(dāng)設(shè)備升級到Android 14的時候,應(yīng)用仍然處于安裝狀態(tài)。

滿足以上三個條件,應(yīng)用仍然有權(quán)限訪問所有的照片和視頻。系統(tǒng)會自動授予應(yīng)用READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO權(quán)限。

  • 從Android 12或更早的版本升級到Android 14
  1. 應(yīng)用安裝在Android 12或更早的版本。
  2. 應(yīng)用擁有READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE 權(quán)限。
  3. 當(dāng)設(shè)備升級到Android 14的時候,應(yīng)用仍然處于安裝狀態(tài)。

滿足以上三個條件,應(yīng)用仍然有權(quán)限訪問所有的照片和視頻。系統(tǒng)會自動授予應(yīng)用READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO權(quán)限。

3.1.7.5 最佳方案

以下是使用READ_MEDIA_VISUAL_USER_SELECTED權(quán)限的最佳方案。

  • 后臺媒體需要新的權(quán)限

當(dāng)應(yīng)用程序進行媒體處理,在后臺壓縮或上傳媒體文件,此時READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO權(quán)限處于被拒絕狀態(tài)。強烈建議添加對READ_MEDIA_VISUAL_USER_SELECTED的支持?;蛘邞?yīng)用應(yīng)該通過使用InputStream或使用ContentResolver來進行查詢是否可以訪問特定的照片或視頻。

  • 不要存儲權(quán)限狀態(tài)

不要通過SharedPreferences或DataStore永久存儲權(quán)限狀態(tài)。存儲的權(quán)限狀態(tài)可能和實際狀態(tài)不同步。權(quán)限狀態(tài)可以在權(quán)限重置、應(yīng)用程序休眠、用戶啟動的應(yīng)用程序設(shè)置更改或應(yīng)用進入后臺更改。相反,請使用ContextCompat.checkSelfPermission()檢查存儲權(quán)限。

  • 有時并不可以完全訪問所有的照片或視頻

基于Android 14引入的變化,應(yīng)用可能只能訪問部分照片庫的內(nèi)容。當(dāng)應(yīng)用查詢使用ContentResolver查詢緩存媒體的數(shù)據(jù),但是緩存的數(shù)據(jù)可能不是最新的。

通過以下方法可以解決不要依賴存儲緩存,要借助ContentResolver直接查詢媒體庫
當(dāng)應(yīng)用在前臺時,將結(jié)果保存到存儲中。

  • URI為臨時訪問

如果用戶在系統(tǒng)權(quán)限對話框中選擇"SELECT PHOTOS AND VIDEOS",則所選取的照片和視頻的訪問權(quán)限的時間是有限的。無論應(yīng)用有沒有權(quán)限,應(yīng)用都應(yīng)該能處理無法訪問任何Uri的情況。

3.1.8 非線性縮放至 200%

從Android 14開始系統(tǒng)支持200%的字體縮放,為低視力用戶提供了符合Web Content Accessibility Guidelines (WCAG)的額外無障礙選項。
為了防止屏幕中大型文本元素縮放過大,系統(tǒng)應(yīng)用非線性縮放曲線。這種縮放策略意味著大文本的縮放速率與小文本不同。非線性字體縮放有助于保持不同大小元素之間的比例層次,同時緩解高度線性文本縮放問題(例如文本被剪切或由于顯示尺寸過大而變的難以閱讀)。

3.1.8.1 使用非線性縮放測試應(yīng)用

如果使用scaled pixel(sp)單位來定義文本大小,那么這些額外的選項和縮放改善將會自動應(yīng)用于應(yīng)用的文本。然而,應(yīng)該在啟動最大字體大?。?00%)的情況下執(zhí)行UI測試,確保應(yīng)用正確使用字體大小,并且可在不影響可用性的情況下適應(yīng)更大的字體。

啟動200%的字體大小,執(zhí)行以下步驟:

  • 打開設(shè)置,找打“輔助功能”>“顯示文本和大小”。
  • 找到字體尺寸選項,按下+按鈕知道最大的縮放尺寸
3.1.8.2 使用scaled pixel(sp)作為文本尺寸單位

請記住始終以sp為單位來指定文本大小。當(dāng)應(yīng)用程序使用sp單位時,Android可以應(yīng)用用戶喜歡的文本大小并適當(dāng)縮放。
不要使用sp作為view之間的距離或高度單位,以sp單位對于非線性縮放,縮放大小將不成比例。例如4sp+20sp并不等于24sp。
3.1.8.3 換算scaled pixel(sp)單位
使用TypedValue.applyDimension()方法將sp單位轉(zhuǎn)為pixels單位,并且使用TypedValue.deriveDimension()方法將pixels單位轉(zhuǎn)為sp單位。這些方法自動應(yīng)用適當(dāng)?shù)姆蔷€性縮放曲線。
避免使用Configuration.fontScale或DisplayMetrics.scaledDensity。因為現(xiàn)在字體縮放是非線性的,這些值將變的不再準(zhǔn)確。

3.2 行為變更:以Android 14為目標(biāo)平臺的應(yīng)用

3.2.1 必須申明前臺服務(wù)類型

為了幫助開發(fā)人員更有意識地定義面向用戶的前臺服務(wù),Android 10 引入了 android:foregroundServiceType 屬性。
如果App以 Android 14 為目標(biāo)平臺,則它必須指定適當(dāng)?shù)那芭_服務(wù)類型。與之前的 Android 版本一樣,可以組合多種類型。可供選擇的前臺服務(wù)類型有:camera、connectedDevice、dataSync、health、location、mediaPlayback、mediaProjection、microphone、phoneCall、remoteMessaging、shortService、specialUse、systemExempted。
如果App中的使用場景與這些類型中的任何一種都不相關(guān),我們強烈建議使用 WorkManager 或user-initiated data transfer jobs。
Android 14新增的前臺服務(wù)類型為health、remoteMessaging、shortService、specialUse 和 systemExempted 類型。
以下代碼片段提供了AndroidManifest.xml中前臺服務(wù)類型聲明的示例:


<manifest ...>
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
    <application ...>
      <service
          android:name=".MyMediaPlaybackService"
          android:foregroundServiceType="mediaPlayback"
          android:exported="false">
      </service>
    </application>
</manifest>復(fù)制

如果面向 Android 14 的應(yīng)用未在AndroidManifest.xml中定義給定服務(wù)的類型,則系統(tǒng)將在為該服務(wù)調(diào)用 startForeground() 時拋出MissingForegroundServiceTypeException。

3.2.1.1 聲明使用前臺服務(wù)類型的新權(quán)限

如果以 Android 14 為目標(biāo)平臺的應(yīng)用使用前臺服務(wù),則它們必須根據(jù)前臺服務(wù)類型聲明 Android 14 引入的特定權(quán)限。所有權(quán)限都定義為normal權(quán)限,并默認(rèn)授予。用戶無法撤銷這些權(quán)限。如果調(diào)用 startForeground() 時未聲明適當(dāng)?shù)那芭_服務(wù)類型權(quán)限,系統(tǒng)將拋出 SecurityException。

3.2.1.2 在運行時包括前臺服務(wù)類型

啟動前臺服務(wù)的應(yīng)用的最佳做法是使用 startForeground() 的重載方法,可以在其中傳入前臺服務(wù)類型的按位整數(shù)??梢赃x擇傳遞一個或多個類型值。
通常,應(yīng)該只聲明特定使用場景所需的類型。這使得更容易滿足系統(tǒng)對每種前臺服務(wù)類型的期望。如果前臺服務(wù)以多種類型啟動,則前臺服務(wù)必須遵守所有類型的平臺實施要求。
但是,如果啟動使用以下任何類型的前臺服務(wù),則每次為該服務(wù)調(diào)用 startForeground() 時都應(yīng)始終包含這些類型:
FOREGROUND_SERVICE_TYPE_CAMERA、FOREGROUND_SERVICE_TYPE_LOCATION、FOREGROUND_SERVICE_TYPE_MICROPHONE。
如果調(diào)用中未指定前臺服務(wù)類型,它將默認(rèn)為AndroidManifest.xml中定義的值。
簡單來說,新特性要求開發(fā)者在使用前臺服務(wù)時,在manifest中聲明前臺服務(wù)類型和所需權(quán)限:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
    <application
    ...
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true"
            android:foregroundServiceType="mediaPlayback" />復(fù)制

也可以在啟動前臺服務(wù)時聲明類型,但此處所聲明的類型應(yīng)是manifest中類型的子集,且最終前臺服務(wù)類型跟隨startForeground中的聲明。
MyService#onStartCommand:


startForeground(222, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);復(fù)制

從而啟動一個前臺服務(wù)。

3.2.1.3 系統(tǒng)運行時檢查

系統(tǒng)檢查是否正確使用了前臺服務(wù)類型,并確認(rèn)應(yīng)用已請求正確的運行時權(quán)限或使用所需的 API。例如,系統(tǒng)期望使用前臺服務(wù)類型 FOREGROUND_SERVICE_TYPE_LOCATION 類型的應(yīng)用請求 ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION。
這意味著應(yīng)用在向用戶請求權(quán)限和啟動前臺服務(wù)時必須遵循非常特定的操作順序。在應(yīng)用嘗試調(diào)用 startForeground() 之前,必須請求并授予權(quán)限。在前臺服務(wù)啟動后請求適當(dāng)權(quán)限的應(yīng)用必須更改此操作順序并在啟動前臺服務(wù)之前請求權(quán)限。
注意:如果應(yīng)用不滿足啟動前臺服務(wù)的所有運行時要求,系統(tǒng)會在為該服務(wù)調(diào)用 startForeground() 后拋出 SecurityException。這會阻止前臺服務(wù)啟動,可能導(dǎo)致正在運行的前臺服務(wù)從前臺進程狀態(tài)中刪除,并可能導(dǎo)致App崩潰。

3.2.1.4 預(yù)期的使用場景與相應(yīng)的服務(wù)類型檢查

為了使用給定的前臺服務(wù)類型,必須在清單文件中聲明特定的權(quán)限,必須滿足特定的運行時要求,并且應(yīng)用必須滿足該類型的一組預(yù)期使用場景。以下部分解釋了必須聲明的權(quán)限、運行時要求以及每種類型的預(yù)期使用場景。

3.2.2 OpenJDK 17 更新

對正則表達式的更改:現(xiàn)在不允許無效的組引用(Invalid group references),以更緊密地遵循 OpenJDK 的語義。您可能會看到 java.util.regex.Matcher 類拋出IllegalArgumentException的新情況,因此請務(wù)必測試應(yīng)用中使用正則表達式的區(qū)域。要在測試時啟用或禁用此更改,請使用兼容性框架工具切換DISALLOW_INVALID_GROUP_REFERENCE 標(biāo)志。
UUID 處理:java.util.UUID.fromString() 方法現(xiàn)在在驗證輸入?yún)?shù)時進行更嚴(yán)格的檢查,因此可能會在反序列化期間出現(xiàn) IllegalArgumentException。要在測試時啟用或禁用此更改,請使用兼容性框架工具切換 ENABLE_STRICT_VALIDATION 標(biāo)志。
ProGuard 問題:在某些情況下,如果嘗試使用 ProGuard 縮小、混淆和優(yōu)化應(yīng)用,則添加 java.lang.ClassValue 類會導(dǎo)致出現(xiàn)問題。該問題源于 Kotlin 庫,該庫根據(jù)Class.forName("java.lang.ClassValue") 是否返回類來更改運行時行為。如果應(yīng)用是針對沒有可用 java.lang.ClassValue 類的舊版本runtime開發(fā)的,則這些優(yōu)化可能會從繼承自 java.lang.ClassValue 的類中刪除 computeValue 方法。

3.2.3 對于隱式意圖和掛起意圖的限制

對于針對Android 14的應(yīng)用程序,Android通過以下方式限制應(yīng)用程序向內(nèi)部組件發(fā)送隱式意圖:

  • 隱式意圖僅發(fā)送給導(dǎo)出的組件。應(yīng)用程序必須使用顯示意圖發(fā)送給未導(dǎo)出的組件,或者將組件標(biāo)記為導(dǎo)出狀態(tài)。
  • 如果一個應(yīng)用創(chuàng)建了一個可變的掛起意圖,而該意圖沒有指定組件或包名,那么系統(tǒng)會拋出一個異常。

這些變化可防止惡意應(yīng)用程序攔截應(yīng)用程序內(nèi)部組件使用的隱式意圖。

例如,下面是應(yīng)用在manifest文件中聲明的意圖過濾器:

<activity
    android:name=".AppActivity"
    android:exported="false">
    <intent-filter>
        <action android:name="com.example.action.APP_ACTION" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>復(fù)制

如果應(yīng)用嘗試使用隱式意圖去啟動這個活動,則會拋出異常:

// Throws an exception when targeting Android 14.
context.startActivity(new Intent("com.example.action.APP_ACTION"));復(fù)制

為了啟動沒有導(dǎo)出的活動,應(yīng)用必須使用顯示意圖:

// This makes the intent explicit.
Intent explicitIntent =
        new Intent("com.example.action.APP_ACTION")
explicitIntent.setPackage(context.getPackageName());
context.startActivity(explicitIntent);復(fù)制

3.2.4 動態(tài)注冊的廣播接收器必須聲明導(dǎo)出標(biāo)志

針對Android 14平臺,在其上面使用上下文注冊接收器的應(yīng)用或服務(wù)需要聲明一個標(biāo)志,以表明接收器是否導(dǎo)出到其他應(yīng)用或設(shè)備。標(biāo)志為RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED。這一要求通過利用Android 13引入的有關(guān)接收器的功能,有助于保護應(yīng)用程序免收安全漏洞的影響。

例如,上下文注冊的廣播接收器聲明RECEIVER_EXPORTED或RECEIVER_NOT_EXPORTED:

// This broadcast receiver should be able to receive broadcasts from other apps.
// This option causes the same behavior as setting the broadcast receiver's
// "exported" attribute to true in your app's manifest.
context.registerReceiver(sharedBroadcastReceiver, intentFilter,
    RECEIVER_EXPORTED);

// For app safety reasons, this private broadcast receiver should **NOT**
// be able to receive broadcasts from other apps.
context.registerReceiver(privateBroadcastReceiver, intentFilter,
    RECEIVER_NOT_EXPORTED);復(fù)制

如果應(yīng)用利用Context#registerReceiver方法注冊了一個接收器,僅對于系統(tǒng)廣播,那么該應(yīng)用不需要聲明以上標(biāo)志。

3.2.5 更安全的動態(tài)代碼加載

如果App以 Android 14 為目標(biāo)平臺并使用動態(tài)代碼加載 (DCL),則所有動態(tài)加載的文件都必須標(biāo)記為只讀。否則,系統(tǒng)會拋出異常。我們建議應(yīng)用盡可能避免動態(tài)加載代碼,因為這樣做會大大增加應(yīng)用因代碼注入或代碼篡改而受到危害的風(fēng)險。
如果必須動態(tài)加載代碼,請使用以下方法將動態(tài)加載的文件(例如 DEX、JAR 或 APK 文件)在文件打開后和寫入任何內(nèi)容之前立即設(shè)置為只讀:

File jar = new File("DYNAMICALLY_LOADED_FILE.jar");
try (FileOutputStream os = new FileOutputStream(jar)) {
    // Set the file to read-only first to prevent race conditions
    jar.setReadOnly();
    // Then write the actual file content
} catch (IOException e) { ... }
PathClassLoader cl = new PathClassLoader(jar, parentClassLoader);復(fù)制

處理已存在的動態(tài)加載文件
為防止現(xiàn)有動態(tài)加載文件拋出異常,我們建議在嘗試在應(yīng)用中再次動態(tài)加載文件之前刪除并重新創(chuàng)建這些文件。重新創(chuàng)建文件時,請按照前面的指導(dǎo)在寫入時將文件標(biāo)記為只讀。或者,可以將現(xiàn)有文件重新標(biāo)記為只讀,但在這種情況下,我們強烈建議首先驗證文件的完整性(例如,通過根據(jù)可信值檢查文件的簽名),以幫助保護應(yīng)用免受惡意操作。

3.2.6 防止zip文件路徑穿透攻擊

對于針對 Android 14 的應(yīng)用,Android 通過以下方式防止 Zip 路徑穿透漏洞:如果 zip 文件條目名稱包含“..”或以“/”開頭,則 ZipFile(String) 和 ZipInputStream.getNextEntry() 會拋出 ZipException。
應(yīng)用可以通過調(diào)用 dalvik.system.ZipPathValidator.clearCallback() 選擇關(guān)閉此驗證。

3.2.7 后臺啟動Activity的附加限制

對于目標(biāo)平臺是Android 14的應(yīng)用,系統(tǒng)進一步限制了應(yīng)用在后臺啟動Activity的權(quán)限。目的是為了防止惡意程序濫用API從后臺啟動破壞性活動來保護用戶。

3.2.7.1 系統(tǒng)允許后臺應(yīng)用啟動Activity的情況

Android 14新的限制主要是限制這些情況。

后臺啟動Activity限制的例外情況主要為:

  • 應(yīng)用具有可見窗口,例如前臺Activity。
  • 應(yīng)用在前臺任務(wù)的返回棧中擁有Activity。
  • 應(yīng)用在Recents屏幕上現(xiàn)有任務(wù)的返回棧中擁有Activity。
  • 應(yīng)用的某個Activity剛在不就前啟動
  • 應(yīng)用最近為某個Activity調(diào)用了finish()。這僅適用于在調(diào)用finish()時,應(yīng)用在前臺或前臺任務(wù)的返回棧中擁有Activity的情況。
  • 應(yīng)用具有受系統(tǒng)約束的服務(wù)。此情況僅適用于以下服務(wù),這些服務(wù)可能需要啟動界面:AccessibilityService、AutofillService、CallRedirectionService、HostApduService、InCallService、TileService、VoiceInteractionService 和 VrListenerService。
  • 應(yīng)用中的某個服務(wù)受另一個可見應(yīng)用約束。請注意,綁定到服務(wù)的應(yīng)用必須保持可見,以便后臺應(yīng)用成功啟動Activity。
  • 應(yīng)用中某個服務(wù)受到另一個可見應(yīng)用約束。請注意,綁定到服務(wù)的應(yīng)用必須保持可見,以便后臺應(yīng)用成功啟動Activity。
  • 應(yīng)用收到系統(tǒng)的PendingIntent通知。對于服務(wù)和廣播接收器的掛起Intent,應(yīng)用可在該掛起Intent發(fā)送幾秒后啟動Activity。
  • 應(yīng)用收到另一個可見應(yīng)用發(fā)送的PendingIntent。
  • 應(yīng)用收到它應(yīng)該在其中啟動界面的系統(tǒng)廣播。包括ACTION_NEW_OUTGOINF_CALL和SECRET_CODE_ACTION。應(yīng)用可在廣播發(fā)送幾秒后啟動Activity。
  • 應(yīng)用通過CompanionDeviceManager API與配套硬件設(shè)備相關(guān)聯(lián)。此API支持應(yīng)用啟動API,以響應(yīng)用戶在配對設(shè)備上執(zhí)行的操作。
  • 應(yīng)用是在設(shè)備所有者模式下運行的設(shè)備政策控制器。示例用例包括完全托管的企業(yè)設(shè)備,以及數(shù)字標(biāo)識牌和自助服務(wù)終端等專用設(shè)備。
  • 用戶已向應(yīng)用授予SYSTEM_ALERT_WINDOW權(quán)限。
3.2.7.2 Android 14新增的兩個限制條件

3.2.7.2.1 新增限制一

當(dāng)后臺應(yīng)用使用PendingIntent#()send()方法,發(fā)送PendingIntent的時候,其相關(guān)的參數(shù)ActivityOptions應(yīng)該設(shè)置setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED)。

使用說明:

// A app創(chuàng)建pendingIntent
PendingIntent pendingIntent = PendingIntent.getActivity(this,
        0,
        notificationIntent,
        PendingIntent.FLAG_MUTABLE,
        null);
//通過將pendingIntent發(fā)送到B app
//B app收到創(chuàng)建的pendingcontent,然后執(zhí)行send()方法發(fā)送,同時構(gòu)建ActivityOptions,
//設(shè)置相應(yīng)的模式
PendingIntent pendingIntent = intent.getParcelableExtra("pending");
ActivityOptions options 
        = ActivityOptions
 .makeBasic()
 .setPendingIntentBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
 .setPendingIntentCreatorBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
pendingIntent.send(context, 0, null, null, null, null, options.toBundle());  復(fù)制
3.2.7.2.2 新增限制二

當(dāng)一個可見應(yīng)用程序使用bindService()方法綁定另一個后臺應(yīng)用程序的服務(wù)時,如果想要授予綁定的后臺服務(wù)所在應(yīng)用啟動Activity,必須要在綁定服務(wù)的時候,即調(diào)用bindService()方法的時候包含BIND_ALLOW_ACTIVITY_STARTS。

使用說明:

targetSdk<Android 14,前臺應(yīng)用綁定后臺服務(wù)啟動Activity需要BIND_AUTO_CREATE
    bindService(intent, coon, BIND_AUTO_CREATE);

targetSdk≥Android 14,前臺應(yīng)用綁定后臺服務(wù)要啟動Activity,需要在之前的基礎(chǔ)上
添加BIND_ALLOW_ACTIVITY_STARTS標(biāo)志。
    bindService(intent, coon, BindServiceFlags.of(BIND_ALLOW_ACTIVITY_STARTS|BIND_AUTO_CREATE));復(fù)制

3.2.8 加強對non-SDK API的限制

從 Android 9(API 級別 28)開始,Android 平臺對應(yīng)用能使用的非 SDK 接口實施了限制。只要應(yīng)用引用非 SDK 接口或嘗試使用反射或 JNI 來獲取其句柄,這些限制就適用。這些限制旨在幫助提升用戶體驗和開發(fā)者體驗,為用戶降低應(yīng)用發(fā)生崩潰的風(fēng)險,同時為開發(fā)者降低緊急發(fā)布的風(fēng)險。
如果應(yīng)用不以 Android 14 為目標(biāo)平臺,其中一些更改可能不會立即產(chǎn)生影響。然而,雖然應(yīng)用目前可以使用一些非 SDK 接口(取決于應(yīng)用的 TargetSDK),但使用任何非 SDK 方法或字段總是會帶來破壞應(yīng)用的高風(fēng)險。
如果不確定應(yīng)用是否使用非 SDK 接口,可以使用 StrictMode API或veridex進行測試。如果應(yīng)用依賴于非 SDK 接口,應(yīng)該開始計劃遷移到 SDK 替代方案。盡管如此,我們了解到某些應(yīng)用具有使用非 SDK 接口的有效使用場景。如果找不到使用非 SDK 接口實現(xiàn)應(yīng)用功能的替代方案,可以向Google請求一個新的公共 API。

3.2.9 收緊全屏intent通知權(quán)限

在 Android 11(API level 30)中,任何應(yīng)用程序都可以使用 Notification.Builder.setFullScreenIntent 在手機鎖定時發(fā)送全屏intent。您可以通過在 AndroidManifest 中聲明 USE_FULL_SCREEN_INTENT 權(quán)限在應(yīng)用安裝時自動授予此權(quán)限。
全屏intent通知專為需要用戶立即關(guān)注的極高優(yōu)先級通知而設(shè)計,例如來電或用戶設(shè)置的鬧鐘。從 Android 14 開始,允許使用此權(quán)限的應(yīng)用僅限于提供通話和鬧鐘的應(yīng)用。
在用戶更新到 Android 14 之前,此權(quán)限對安裝在手機上的應(yīng)用程序保持啟用狀態(tài)。用戶可以打開和關(guān)閉此權(quán)限。
您可以使用新的 API NotificationManager.canUseFullScreenIntent 來檢查App是否具有權(quán)限;如果沒有,App可以使用新的意圖 ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT 來啟動設(shè)置頁面,用戶可以在其中授予權(quán)限。

3.2.10 數(shù)據(jù)安全信息更加可見

3.2.10.1 權(quán)限方面的考慮

對于某些權(quán)限,系統(tǒng)運行時權(quán)限對話框現(xiàn)在包含一個可點擊的部分,突出顯示您的應(yīng)用程序的數(shù)據(jù)共享操作。系統(tǒng)對話框的這一部分包含如下信息,例如您的應(yīng)用可能決定與第三方共享數(shù)據(jù)的原因,以及將用戶鏈接到他們可以控制您的應(yīng)用數(shù)據(jù)訪問的位置。

3.2.10.2 系統(tǒng)通知

如果用戶在App中分享了他們的位置,并且App隨后通過以下方式之一擴大了其位置共享的使用范圍,則用戶會在 30 天內(nèi)看到系統(tǒng)通知:

  • App開始與第三方共享位置數(shù)據(jù)。
  • App開始共享位置數(shù)據(jù)以用于廣告相關(guān)目的。

當(dāng)用戶點擊此通知時,他們將被帶到一個新的位置數(shù)據(jù)共享更新頁面,該頁面顯示了進行相關(guān)更改的應(yīng)用程序的詳細列表,以及更改每個應(yīng)用程序權(quán)限設(shè)置的簡便方法。下圖顯示了此流程的示例。
新的位置數(shù)據(jù)共享更新頁面可從設(shè)備的Settings > Privacy或者Settings > Security & Privacy頁面永久訪問,并顯示最近增加的位置數(shù)據(jù)共享。

當(dāng)某些已安裝的應(yīng)用程序更改其數(shù)據(jù)共享范圍時出現(xiàn)的系統(tǒng)通知

4.遷移指南

在每個Android版本中,都會引入新功能和行為更改,其目的是使Android更有用、更安全且性能更高。在許多情況下,應(yīng)用在新版本上可直接使用,完全符合預(yù)期,但是在某些情況下,需要更新應(yīng)用以適配平臺的變化。
源碼發(fā)布到AOSP(Android開源項目),用戶就可以開始使用新平臺。因此讓應(yīng)用做好準(zhǔn)備,按用戶預(yù)期的方式運行,并理想地利用新功能和API發(fā)揮平臺最大優(yōu)勢。

遷移有兩個典型的階段,可以同時進行:

  • 確保應(yīng)用兼容性(在Android 14最終版本發(fā)布前)
  • 針對新平臺的功能和API調(diào)整應(yīng)用(在最終版本發(fā)布后盡快進行)

4.1 確保與Android 14的兼容

您必須測試現(xiàn)有應(yīng)用在Android 14上的運行表現(xiàn),確保用戶在最新Android版本獲得良好的體驗。有些平臺的更改可能會影響到應(yīng)用的表現(xiàn)。因此,有必要進行全面測試應(yīng)用并進行必要的調(diào)整。
通常您可以調(diào)整應(yīng)用并發(fā)布更新,而無需更改應(yīng)用的targetSdkVersion。同樣,根據(jù)應(yīng)用的構(gòu)建方式及其使用平臺功能的不同,您可能也不需要使用新的API或更改應(yīng)用的compileSdkVersion。
在開始測試強,請務(wù)必熟悉應(yīng)用行為變更。即使不更改應(yīng)用的targetSdkVersion,這些變更也會影響到您的應(yīng)用。

執(zhí)行兼容性測試
大多數(shù)情況下,測試Android 14的兼容性與普通應(yīng)用程序的測試類似。這也是回顧核心應(yīng)用程序質(zhì)量指南和測試最佳實踐的時機。
要進行測試,請在運行Android 14的設(shè)備安裝您當(dāng)前發(fā)布的應(yīng)用,并在查找問題的同時完成所有流程和功能測試。為了集中精力測試,請查看Android 14中引入的所有應(yīng)用程序的行為變化。這些變化可能會影響您的應(yīng)用并可能導(dǎo)致應(yīng)用崩潰。
同時還要確保檢查和測試受限制的非SDK接口的使用。您應(yīng)該將應(yīng)用使用的任何受限接口替換為公共SDK或等效的NDK接口。留意突出顯示這些訪問權(quán)限的logcat警告,并使用StrictMode方法 detectNonSdkApiUsage()來捕獲它們。
最后,請務(wù)必完整測試應(yīng)用中的庫和SDK,確保它們在Android 14上面如期運行,并遵循隱私權(quán)、性能、UX、用戶體驗、數(shù)據(jù)處理和權(quán)限方面的最佳實踐。如果您遇到問題,請嘗試更新到最新版本的SDK,或者聯(lián)系SDK開發(fā)者尋求幫助。
當(dāng)您完成測試并更新后,我們建議您立即發(fā)布兼容的應(yīng)用。這樣可以盡早讓您的用戶測試應(yīng)用,并幫助用戶順利過渡到Android 14。

4.2 更新應(yīng)用的目標(biāo)平臺并使用新的API進行構(gòu)建

一旦發(fā)布應(yīng)用兼容版本后,下一步通過更新targeSdkVersion并利用Android 14新API和功能來添加對Android 14的全面支持。準(zhǔn)備就緒后,即可開始進行這些更新。
當(dāng)您計劃全面支持Android 14的時,請查看以Android 14為目標(biāo)平臺的應(yīng)用的行為變更。這些針對性的行為變更可能會導(dǎo)致功能問題,然后需要解決這些問題。在某些情況下,這些變更需要進行大量的開發(fā)工作,因此我們建議您盡早了解并解決這些問題。為確定影響您的應(yīng)用的具體的行為變更,請使用兼容性開關(guān)來測試已啟用所選變更的應(yīng)用。

以下是全面支持Android 14的步驟。

  • 獲取SDK、更改目標(biāo)平臺并使用新API進行構(gòu)建

如需要測試完整的Android 14的支持,請使用最新預(yù)覽版的Android Studio下載Android 14SDK,以及其他所需要的任何工具。接下來,更新應(yīng)用的targetSdkVersion和compileSdkVersion,然后重新編譯該應(yīng)用。有關(guān)詳細信息,請參閱SDK設(shè)置指南。

  • 測試Android 14應(yīng)用

編譯應(yīng)用并將其安裝在運行Android 14的設(shè)備上后,開始測試,確保應(yīng)用在Android 14平臺上正常運行。某些行為變換僅在應(yīng)用以新平臺為目標(biāo)平臺時適用,因此在開始之前需要查看這些行為變更。

與基本測試一樣,完成所有流程和功能以查找問題。將測試重點放在以Android 14為目標(biāo)平臺的行為變更上。您還可以根據(jù)核心應(yīng)用質(zhì)量指南測試最佳實踐來檢查應(yīng)用。

確保檢查和測試可能使用的受限非SDK接口的使用。留意高亮顯示這些訪問權(quán)限的logcat警告,并使用StrictMode方法 detectNonSdkApiUsage()以編程方式來捕獲它們。

最后,請務(wù)必完整測試應(yīng)用中的庫和SDK,確保它們在Android 14上面如期運行,并遵循隱私權(quán)、性能、UX、用戶體驗、數(shù)據(jù)處理和權(quán)限方面的最佳實踐。如果您遇到問題,請嘗試更新到最新版本的SDK,或者聯(lián)系SDK開發(fā)者尋求幫助。

  • 切換應(yīng)用兼容性測試開關(guān)進行測試

Android 14包含兼容性開關(guān),可讓您更輕松地在應(yīng)用中測試針對性的行為變更。對于可調(diào)試的應(yīng)用程序,切換開關(guān)可讓您:在不實際更改應(yīng)用程序的targetSdkVersion的情況下測試針對性的變更。您可以切換開關(guān)強制啟用特定的針對性行為變更,以評估對現(xiàn)有應(yīng)用的影響。
僅針對特定變更進行測試。您可以使用切換開關(guān)停用除了要測試的變更之外的所有針對性變更,而不必一次性處理所有針對性變更。
通過adb管理切換開關(guān)。您可以使用adb命令在自動測試環(huán)境中啟用和停用可切換的變更。
使用標(biāo)準(zhǔn)變更ID更快地進行調(diào)試。每個可切換的變更都具有唯一ID和名稱,可用于在日志輸出中快速調(diào)試出根本原因。
在您準(zhǔn)備更改應(yīng)用目標(biāo)平臺時,或者在積極開發(fā)以便支持Android 14時,切換開關(guān)將會十分有用。如需了解更多信息,請參閱兼容性框架變更(Android 14)。

5.重點適配問題

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

相關(guān)閱讀更多精彩內(nèi)容

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