Categories
程式開發

攜程Android 10適配踩坑指南


2019 年 9 月 3 日,Google 發布了 Android 10 正式版。 Android 10 聚焦移動創新、安全隱私和數字健康三大主題,全面打造最佳用戶體驗。

攜程Android 10適配踩坑指南 1

背景

目前攜程旅行線上最新版本已適配到Android 10(API =29),由於從API=26升級到API=29,跨度較大,我們提前對相關適配進行了調研,希望其中一些經驗能對其他開發者有一定的幫助。

在Android 10 版本中,官方的改動較大,相應的開發者​​適配成本還是很高的。基於前期調研,我們主要基於以下幾方面進行Android 10的適配:

  • Android X
  • 分區存儲
  • 設備ID
  • 明文HTTP限制

一、AndroidX

AndroidX 對原始 Android Support庫進行了重大改進,後者現在已不再維​​護。 AndroidX 軟件包完全取代了支持庫,不僅提供同等的功能,而且提供了新的庫。

1.1 什麼是AndroidX

Android系統在剛剛面世的時候,可能連它的設計者也沒有想到它會如此成功。隨著Android系統版本不斷地迭代更新,每個版本中都會加入很多新的API進去,但是新增的API在老版系統中並不存在,因此這就出現了一個向下兼容的問題。

於是Android團隊推出了一個鼎鼎大名的Android Support Library,用於提供向下兼容的功能。比如我們熟知的support-v4庫,appcompat-v7庫都是屬於Android Support Library的。 4在這裡指的是Android API版本號,對應的系統版本是1.6。 support-v4的意思就是這個庫中提供的API會向下兼容到Android 1.6系統。類似地,appcompat-v7指的是將庫中提供的API向下兼容至API 7,也就是Android 2.1系統。

隨著時間的推移,Android1.6、2.1系統早已被淘汰了,現在Android官方支持的最低系統版本已經是4.0.1,對應的API版本號是15。 support-v4、appcompat-v7庫也不再支持那麼久遠的系統了,但是它們的名字卻一直保留了下來,雖然它們現在的實際作用已經對不上當初命名的原因了。

Android團隊也意識到這種命名已經非常不合適了,於是對這些API的架構進行了一次重新的劃分,推出了AndroidX。因此,AndroidX本質上其實就是對Android Support Library進行的一次升級。

1.2 為什麼要升級AndroidX

  • 版本 28.0.0 是Android Support 庫的最後一個版本。官方將不再發布 android.support 庫版本。所有新功能都將在 AndroidX命名空間中開發。
  • 長遠來看。 AndroidX重新設計了包結構,旨在鼓勵庫的小型化,支持庫和架構組件包的名字進行了簡化。而且這也是減輕Android生態系統碎片化的有效方式。
  • 與Android Support庫不同,AndroidX軟件包是單獨維護和更新的。這些AndroidX包使用嚴格的語義版本控制,從版本1.0.0開始,您可以單獨更新項目中的AndroidX庫。

1.3 適配步驟

1.3.1 環境準備

  • AndroidStudio 3.2.0+
  • gradle:gradle-4.6+

另外修改相關app、library模塊中build.gradle的compileSdkVersion、targetSdkVersion、buildToolsVersion的配置,都設置為29,示例如下:

android {
   compileSdkVersion 29
   buildToolsVersion 29.0.2
   defaultConfig {
      targetSdkVersion 29
   }
   ...
}

1.3.2 修改當前項目的 gradle.properties

android.useAndroidX=true
android.enableJetifier=true

其中:

  • android.useAndroidX=true 表示當前項目啟用 AndroidX;
  • android.enableJetifier=true 表示將依賴包也遷移到AndroidX 。如果取值為 false ,表示不遷移依賴包到AndroidX,但在使用依賴包中的內容時可能會出現問題,如果你的項目中沒有使用任何三方依賴,此​​項可以設置為 false。

1.3.3 修改項目中的build.gradle依賴庫

implementation 'com.android.support:appcompat-v7:28.0.0'
→ implementation 'androidx.appcompat:appcompat:1.0.2'

implementation 'com.android.support:design:28.0.0'
→implementation 'com.google.android.material:material:1.0.0'

implementation 'com.android.support.constraint:constraint-layout:1.1.3'
→ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

映射關係:

https://developer.android.com/jetpack/androidx/migrate/artifact-mappings

1.3.4 修改支持庫類

將原來import的android.包刪除,重新import新的androidx.包;

import android.support.v7.app.AppCompatActivity; →import androidx.appcompat.app.AppCompatActivity;

1.3.5 遷移

官方遷移指南:

https://developer.android.com/jetpack/androidx/migrate#migrate

在 AndroidStudio 3.2 或更高版本(截圖中 AndroidStudio 為 3.5 版本)中執行如下操作:菜單>Refactor > Migrate to AndroidX(如果遷移失敗,就需要重複上面1,2,3,4步手動去修改遷移)

攜程Android 10適配踩坑指南 2

注意:

  • 使用AS遷移工具並不能完全修改完畢,需要手動修改
  • support包名涉及到資源修改,切記檢查資源中的類路徑

二、分區存儲

2.1 背景介紹

為了更好的保護用戶數據並限制設備冗余文件增加,以Android 10(API 級別29)及更高版本為目標平台的應用在默認情況下被賦予了對外部存儲設備的分區訪問權限(即分區存儲), 對外部存儲文件訪問方式重新設計,便於用戶更好的管理外部存儲文件。

應用只能看到本應用專有的目錄(通過 Context.getExternalFilesDir() 訪問)以及特定類型的媒體。除非您的應用需要訪問存放在應用的專有目錄以及 MediaStore 之外的文件,否則最好使用分區存儲。

要點:

  • Android Q文件存儲機制修改成了沙盒模式
  • APP只能訪問自己目錄下的文件和公共媒體文件
  • Android Q版本以下機型,還是使用老的文件存儲方式
  • Android Q及以上版本機型,所有應用均需要分區存儲, 所以應用需要提前確保支持分區存儲

需要注意:在適配AndroidQ的時候還要兼容Q系統版本以下的,使用SDK_VERSION區分

2.2 新特性概覽

2.2.1 外部存儲

外部存儲被分為應用私有目錄以及共享目錄兩個部分:

  • 應用私有目錄:存儲應用私有數據,外部存儲應用私有目錄對應Android/data/packagename,內部存儲應用私有目錄對應data/data/packagename;
  • 共享目錄:存儲其他應用可訪問文件, 包含媒體文件、文檔文件以及其他文件,對應設備DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目錄

1)私有目錄

應用私有目錄文件訪問方式與之前Android版本一致,可以通過File path獲取資源。

2)共享目錄

共享目錄文件需要通過MediaStore API或者Storage Access Framework方式訪問。

  • MediaStore API在共享目錄指定目錄下創建文件或者訪問應用自己創建文件,不需要申請存儲權限
  • MediaStore API訪問其他應用在共享目錄創建的媒體文件(圖片、音頻、視頻), 需要申請存儲權限,未申請存儲權限,通過ContentResolver查詢不到文件Uri,即使通過其他方式獲取到文件Uri,讀取或創建文件會拋出異常;
  • MediaStore API不能夠訪問其他應用創建的非媒體文件(pdf、office、doc、txt等), 只能夠通過Storage Access Framework方式訪問;

2.3 受影響的變更

2.3.1 圖片位置信息

一些圖片會包含位置信息,因為位置對於用戶屬於敏感信息, Android 10應用在分區存儲模式下圖片位置信息默認獲取不到,應用通過以下兩項設置可以獲取圖片位置信息:

  • 在manifest中申請ACCESS_MEDIA_LOCATION
  • 調用MediaStore setRequireOriginal(Uri uri)接口更新圖片Uri

2.3.2 訪問數據

MediaStore.Files應用分區存儲模式下,MediaStore.Files 集合只能夠獲取媒體文件信息(圖片、音頻、視頻), 獲取不到非media(pdf、office、doc、txt等)文件。

2.3.3 File Path路徑訪問受影響接口

開啟分區存儲新特性, Andrioid 10不能夠通過File Path路徑直接訪問共享目錄下資源,以下接口通過File 路徑操作文件資源,功能會受到影響,應用需要使用MediaStore或者SAF方式訪問。

類名稱 受影響的接口
File createNewFile()
delete()
renameTo(File dest)
mkdir()
mkdirs()
FileInputStream FileInputStream(File file)
FileInputStream(String name)
FileOutputStream FileOutputStream(String name)
FileOutputStream(String name, boolean append)
FileOutputStream(File file)
FileOutputStream(File file, boolean append)
BitmapFactory decodeFile(String pathName)
decodeFile(String pathName, Options opts)

2.3.4 存儲特性Android版本差異概覽

存儲位置 路徑 版本 存儲權限
內部存儲 data/data/packagename 所有 getFilesDir()、getCacheDir()
外部存儲 私有目錄 Android/data/packagename 4.4以上 getExternalFilesDir()、getExternalCacheDir()、SAF
共享目錄 DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download <10 Environment.getExternalStorageDirectory()
SAF
>=10 訪問其他應用media文件 –>MediaStore API
訪問其他應用創建的非media文件 –> SAF
訪問自己應用創建的文件 –>MediaStore API
SAF

2.4 兼容模式

應用未完成外部存儲適配工作,可以臨時以兼容模式運行, 兼容模式下應用申請存儲權限,即可擁有外部存儲完整目錄訪問權限,通過Android10之前文件訪問方式運行,以下兩種方法設置應用以兼容模式運行。

2.4.1 AndroidManifest中申明

tagretSDK 大於等於Android 10(API level 29), 在manifest中設置requestLegacyExternalStorage屬性為true。


...

...

2.4.2、判斷兼容模式接口

//返回值
//true : 应用以兼容模式运行
//false:应用以分区存储特性运行
Environment.isExternalStorageLegacy();

備註:應用已完成存儲適配工作且已打開分區存儲開關,如果當前應用以兼容模式運行,覆蓋安裝後應用仍然會以兼容模式運行,卸載重新安裝應用才會以分區存儲模式運行

2.5 適配方案

2.5.1 方案概覽

分區存儲適配包含文件遷移以及文件訪問兼容性適配兩個部分:

1)文件遷移

文件遷移是將應用共享目錄文件遷移到應用私有目錄或者Android10要求的media集合目錄。

  • 針對只有應用自己訪問並且應用卸載後允許刪除的文件,需要遷移文件到應用私有目錄文件,可以通過File path方式訪問文件資源,降低適配成本。
  • 允許其他應用訪問,並且應用卸載後不允許刪除的文件,文件需要存儲在共享目錄,應用可以選擇是否進行目錄整改,將文件遷移到Android10要求的media集合目錄。

2)文件訪問兼容性

共享目錄文件不能夠通過File path方式讀取,需要使用MediaStore API或者Storage Access Framework框架進行訪問。

2.5.2 適配指導

AndroidQ中使用ContentResolver進行文件的增刪改查。

1)獲取(創建)私有目錄下的文件夾

//在自身目录下创建apk文件夹
File apkFile = context.getExternalFilesDir("apk");

2)創建私有目錄文件

生成需要下載的路徑,通過輸入輸出流讀取寫入

String apkFilePath = context.getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "demo.apk");
OutputStream os = null;
try {
    os = new FileOutputStream(newFile);
    if (os != null) {
        os.write("file is created".getBytes(StandardCharsets.UTF_8));
        os.flush();
    }
} catch (IOException e) {
} finally {
    try {
        if (os != null) {
        os.close();
    }catch (IOException e1) {
    }
}

3)創建共享目錄文件夾

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    ContentResolver resolver = context.getContentResolver();
    ContentValues values = new ContentValues();
    values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
    values.put(MediaStore.Downloads.DESCRIPTION, fileName);
    //设置文件类型
    values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
    //注意MediaStore.Downloads.RELATIVE_PATH需要targetVersion=29,
    //故该方法只可在Android10的手机上执行
    values.put(MediaStore.Downloads.RELATIVE_PATH, "Download" + File.separator + "apk");
    Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
    Uri insertUri = resolver.insert(external, values);
    return insertUri;
}else{
    ...
}

4)在共享目錄指定文件夾下創建文件

主要是在公共目錄下創建文件或文件夾拿到本地路徑uri,不同的Uri,可以保存到不同的公共目錄中。接下來使用輸入輸出流就可以寫入文件。

重點:AndroidQ中不支持file://類型訪問文件,只能通過uri方式訪問。

/**
  * 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法
  */
private Uri  createImageUri() {
    String status = Environment.getExternalStorageState();
    // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
    if (status.equals(Environment.MEDIA_MOUNTED)) {
        return getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
    } else {
        return getContext().getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
    }
}

5)通過MediaStore API讀取公共目錄下的文件

if (cursor != null && cursor.moveToFirst()) {
    do {
        ...
        int _id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
        Uri imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, _id);
        ...
    } while (!cursor.isLast() && cursor.moveToNext());
} else {
...
}
// 通过uri获取bitmap
public Bitmap getBitmapFromUri(Context context, Uri uri) {
    ParcelFileDescriptor parcelFileDescriptor = null;
    FileDescriptor fileDescriptor = null;
    Bitmap bitmap = null;
    try {
        parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
        if (parcelFileDescriptor != null && parcelFileDescriptor.getFileDescriptor() != null) {
            fileDescriptor = parcelFileDescriptor.getFileDescriptor();
            //转换uri为bitmap类型
            bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        try {
            if (parcelFileDescriptor != null) {
            parcelFileDescriptor.close();
        }catch (IOException e) {
        }
    }
    return bitmap;
}

6)使用MediaStore刪除文件

context.getContentResolver().delete(fileUri, null, null);

三、設備ID

從Android 10開始已經無法完全標識一個設備,曾經用mac地址、IMEI等設備信息標識設備的方法,從Android 10開始統統失效。而且無論你的APP是否適配過Android 10。

3.1 IMEI等設備信息

從Android10開始普通應用不再允許請求權限android.permission.READ_PHONE_STATE。而且,無論你的App是否適配過Android Q(既targetSdkVersion是否大於等於29),均無法再獲取到設備IMEI等設備信息。

受影響的API:

Build.getSerial();
TelephonyManager.getImei();
TelephonyManager.getMeid()
TelephonyManager.getDeviceId();
TelephonyManager.getSubscriberId();
TelephonyManager.getSimSerialNumber();
  • targetSdkVersion<29 的應用,其在獲取設備ID時,會直接返回null
  • targetSdkVersion>=29 的應用,其在獲取設備ID時,會直接拋出異常SecurityException

如果您的App希望在Android 10以下的設備中仍然獲取設備IMEI等信息,可按以下方式進行適配:


3.2 Mac地址隨機分配

從Android10開始,默認情況下,在搭載 Android 10 或更高版本的設備上,系統會傳輸隨機分配的 MAC 地址。 (即從Android 10開始,普通應用已經無法獲取設備的真正mac地址,標識設備已經無法使用mac地址)

3.3 如何標識設備唯一性

3.3.1 Google解決方案:

如果您的應用有追踪非登錄用戶的需求,可用ANDROID_ID來標識設備。

  • ANDROID_ID生成規則:簽名+設備信息+設備用戶
  • ANDROID_ID重置規則:設備恢復出廠設置時,ANDROID_ID將被重置
String androidId = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID);

3.3.2 信通院統一SDK(OAID)

統一標識依據電信終端產業協會(TAF)、移動安全聯盟(MSA)聯合推出的團體標準《移動智能終端補充設備標識規範》開發,移動智能終端補充設備標識體系統一調用SDK 集成設備廠商提供的接口,並獲得主流設備廠商的授權。

移動安全聯盟(MSA)組織中國信息通信研究院(以下簡稱“中國信通院”)與終端生產企業、互聯網企業共同研究制定了“移動智能終端補充設備標識體系”,定義了移動智能終端補充設備標識體系的體系架構、功能要求、接口要求以及安全要求,使設備生產企業統一開發接口,為移動應用開發者提供統一調用方式,方便移動應用接入,降低維護成本。

1)SDK獲取

MSA 統一 SDK 下載地址:

移動安全聯盟官網,http://www.msa-alliance.cn/

2)接入方式

  • 解壓miit_mdid_sdk_v1.0.13.rar,
  • 把 miit_mdid_1.0.13.aar 拷貝到項目中,並設置依賴。
  • 將 supplierconfig.json 拷貝到項目 assets 目錄下,並修改裡邊對應 內容,特別是需要設置 appid 的部分。需要設置 appid 的部分需要去對應的廠 商的應用商店裡註冊自己的 app。
{
  "supplier":{
    "xiaomi":{
      "appid":"***"
    },
    "huawei":{
      "appid":"***"
    }
    ...
  }
}
  • 在初始化方法中調用JLibrary.InitEntry
try {
    JLibrary.InitEntry(FoundationContextHolder.getContext());
} catch (Throwable e) {
}
  • 實例化MSA SDK
public static void initMSASDK(Context context){
    int code = 0;
    try {
        code =  MdidSdkHelper.InitSdk(context,true,listener);
        if (code == ErrorCode.INIT_ERROR_MANUFACTURER_NOSUPPORT){//1008611,不支持的厂商
        }else if (code == ErrorCode.INIT_ERROR_DEVICE_NOSUPPORT){//1008612,不支持的设备
        }else if (code == ErrorCode.INIT_ERROR_LOAD_CONFIGFILE){//1008613,加载配置文件失败
        }else if (code == ErrorCode.INIT_ERROR_RESULT_DELAY){//1008614,信息将会延迟返回,获取数据可能在异步线程,取决于设备
        }else if (code == ErrorCode.INIT_HELPER_CALL_ERROR){//1008615,反射调用失败
        }
        //code可记录异常供分析
    }catch (Throwable throwable){
    }
}

static IIdentifierListener listener = new IIdentifierListener() {
    @Override
    public void OnSupport(boolean support, IdSupplier idSupplier) {
        try{
            isSupport  = support;
            if (null != idSupplier && isSupport){
                //是否支持补充设备标识符获取
                oaid = idSupplier.getOAID();
                aaid = idSupplier.getAAID();
                vaid = idSupplier.getVAID();
            }else {
                ...
            }
        }catch (Exception e){
        }
    }
};
  • 通過以上方法獲取到OAID等設備標識之後,即可作為唯一標識使用。

四、明文HTTP限制

當SDK版本大於API 28時,默認限制了HTTP請求,並出現相關日誌“java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy“。

該問題有兩種解決方案:

1)在AndroidManifest.xml中Application節點添加如下代碼


2)在res目錄新建xml目錄,已建的跳過 在xml目錄新建一個xml文件network_security_config.xml,然後在AndroidManifest.xml中Application添加如下節點代碼。

android:networkSecurityConfig="@xml/network_config"

network_config.xml(命名隨機)



    

五、展望

2020年2月20號,Google提前發布了Android 11預覽版,通過 5G、折疊屏、內置機器學習等新技術,照亮了移動設備的未來。 Android 11 依然致力於讓用戶暢享最新科技,並始終確保將安全和隱私放在首位,幫助用戶管理敏感數據和文件的訪問權限。此外還對平台的關鍵區域做出了強化,以保持操作系統的彈性和安全性。

對於像Android這樣的開放性OS來說,佔有的市場份額越大,整個Android生態系統的發展會越好。隨著Android對於碎片化的整理、用戶隱私和安全性的重視、5G和機器學習等新技術的引入,已逐步抓住快速增長的中產階級用戶,未來的市場份額增長量將是不可預估的。

參考文檔:

1、AndroidX 概覽

https://developer.android.google.cn/jetpack/androidx

2、Android 10介紹

https://developer.android.com/about/versions/10

3、Android 11預覽版介紹

https://developer.android.com/preview

4、Android Q Adaptation Guide

https://chinesefoodstudio.com/index.php/2019/11/21/android-q-adaptation-guide/

5、Android 10分區存儲介紹及百度APP適配實踐

https://segmentfault.com/a/1190000021760036

作者介紹

曙光,攜程資深軟件工程師,負責市場營銷相關研發及管理工作。

本文轉載自公眾號攜程技術(ID:ctriptech)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697269503&idx=2&sn=f5505724dc​​ee64ebd9904ee16a2bfedb&chksm=8376efcbb40166ddf0f301003b0c05b89f110957fa0872c8ba741cb49b61c404ce849c769978&scene=27#wechat_redirect