Android10(Api 29)新特性適配小結

文檔說明

  1. 本文檔主要對影響比較大的部分進行簡單總結,內容並不全面;

  2. 本文檔基於谷歌AndroidQ官方文檔華爲Q版本應用兼容性整改指導(華爲的有點過時);

  3. 所用測試機:Google初代Pixel,AndroidQ-beta6-190730.005

  4. 版本號對應關係

    Android-Q = Android-10 = Api29

    Android-P = Android-9.0 = Api28

設備硬件標識符訪問限制

限制應用訪問不可重設的設備識別碼,如 IMEI、序列號等,系統應用不受影響。

原來的做法

// 在AndroidQ上以下方法都會有問題
// 返回:866976045261713; 
TelephonyManager tm = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
tm.getDeviceId();
tm.getSubscriberId();
tm.getDeviceId(TelephonyManager.PHONE_TYPE_NONE);
//返回:66J0218B19000977; 
Build.getSerial();
  1. 在低於AndroidQ的系統上沒問題

  2. 在AndroidQ及以上的系統上運行時:

    • targetSdkVersion<Q,返回null或“unknown”;

    • targetSdkVersion>=Q,拋異常:

      SecurityException: getDeviceId: The user 10196 does not meet the requirements to access device identifiers.

  3. 受影響的方法

    • Build
      • getSerial()
    • TelephonyManager
      • getImei()
      • getDeviceId()
      • getMeid()
      • getSimSerialNumber()
      • getSubscriberId()

替代方案

  1. 方案一:

    使用AndroidId代替,缺點是應用簽署密鑰或用戶(如系統恢復出產設置)不同返回的Id不同。與實際測試結果相符。
    經實際測試:相同簽名密鑰的不同應用androidId相同,不同簽名的應用androidId不同。恢復出產設置或升級系統沒測。

    // 返回:496836e3a48d2d9d
    String androidId = Settings.System.getString(context.getContentResolver(),
            Settings.Secure.ANDROID_ID);
    
  2. 方案二:

    通過硬件信息拼接,缺點是還是不能保證唯一。
    經測試:似乎與方案一比更穩定,不受密鑰影響,但非官方建議,沒安全感。

    private static String makeDeviceId(Context context) {
        String  deviceInfo = new StringBuilder()
                .append(Build.BOARD).append("#")
                .append(Build.BRAND).append("#")
                //CPU_ABI,這個值和appp使用的so庫是arm64-v8a還是armeabi-v7a有關,捨棄
                //.append(Build.CPU_ABI).append("#")
                .append(Build.DEVICE).append("#")
                .append(Build.DISPLAY).append("#")
                .append(Build.HOST).append("#")
                .append(Build.ID).append("#")
                .append(Build.MANUFACTURER).append("#")
                .append(Build.MODEL).append("#")
                .append(Build.PRODUCT).append("#")
                .append(Build.TAGS).append("#")
                .append(Build.TYPE).append("#")
                .append(Build.USER).append("#")
                .toString();
        try {
        	//22a49a46-b39e-36d1-b75f-a0d0b9c72d6c
           return UUID.nameUUIDFromBytes(deviceInfo.getBytes("utf8")).toString();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String androidId = Settings.System.getString(context.getContentResolver(),
                    Settings.Secure.ANDROID_ID);
        return androidId;
    }
    

禁止後臺啓動Activity

官方文檔

情況描述

  1. AndroidQ上,後臺啓動Activity會被系統忽略,不管targetSdkVersion多少;
  2. AndroidQ上,即使應用有前臺服務也不行;
  3. AndroidQ以下版本沒影響。

解決方法

發送全屏通知:

//AndroidManifest 聲明新權限,不用動態申請
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>

Intent intent = new Intent(this, ScopedStorageActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
        REQ_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(this, Constants.CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_launcher_foreground)
        .setContentTitle("Incoming call")
        .setContentText("(919) 555-1234")
        .setPriority(NotificationCompat.PRIORITY_HIGH)
        .setCategory(NotificationCompat.CATEGORY_ALARM)
        //設置全屏通知後,發送通知直接啓動Activity
        .setFullScreenIntent(pendingIntent, true)
        .build();
NotificationManager manager = getSystemService(NotificationManager.class);
manager.notify(445456, notification);

但是:在華爲mate20(Api-28)上需要到設置中打開橫幅通知;原生AndroidQ(beta6)上有效。

後臺應用增加定位限制

官方文檔

情況描述

  1. 後臺應用要獲取位置信息需要動態申請權限,

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

  2. 在AndroidQ上運行:

    • targetSdkVersion<Q,沒影響,申請權限時系統默認會加上後臺位置權限
    • targetSdkVersion>=Q,需申請;
    • 應用變爲後臺應用90s後開始定位失敗(Pixel AndroidQ-beta6)
  3. ACCESS_BACKGROUND_LOCATION不能單獨申請,需要和ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION一起申請

解決方法

  1. 動態申請即可;

  2. 啓動前臺服務

    <!-需要設置foregroundServiceType爲“location” ->
    <service 
     android:name=".permission.LocationService"
        android:foregroundServiceType="location"/>
    

分區存儲

官方文檔

情況描述

  1. 從Android10開始應用將不可直接訪問外部存儲(/sdcard)文件,否則拋異常。
  2. 在AndroidQ上運行:
    • targetSdkVersion<Q,沒影響;
    • targetSdkVersion>=Q,默認啓用過濾視圖,應用以外的文件需要通過存儲訪問框架(SAF,StorageAccessFramework)讀寫。

解決方法

方法一、停用過濾視圖,使用舊版存儲模式
   <manifest ... >
     <!-- This attribute is "false" by default on apps targeting Android Q. -->
     <application android:requestLegacyExternalStorage="true" ... >
       ...
     </application>
   </manifest>
方法二、將文件存儲到過濾視圖中,官方推薦。
   // /Android/data/com.example.androidq/files/Documents
   File dir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);

優點:不用申請讀寫權限;

缺點:隨應用卸載而刪除;

方法三、使用存儲訪問框架(SAF),由用戶指定要讀寫的文件。

​ 這個功能Android 4.4(API: 19)就有,官方文檔在此

方法四、獲取用戶指定的某個目錄的讀寫權限

​ 從Android5.0(Api 21)開始就有,官方文檔
步驟

1. 申請目錄的訪問權限

會打開系統的文件目錄,由用戶自己選擇允許訪問的目錄,不用申請WRITE/READ_EXTERNAL_STORAGE權限。

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | 
        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);				
startActivityForResult(intent, REQ_CODE);

執行上述代碼後會出現類似如下圖界面,點擊‘允許訪問“DuoKan”’按鈕,
允許訪問權限
允許了之後通過onActivityResult()intent.getData()得到該目錄的Uri,通過Uri可獲取子目錄和文件。這種方式的缺點是應用重裝後權限失效,即使可以保存了這個Uri也沒用。

Uri dirUri = intent.getData();
// 持久化;應用重裝後權限失效,即使知道這個uri也沒用
SPUtil.setValue(this, SP_DOC_KEY, dirUri.toString());
//重要:少這行代碼手機重啓後會失去權限
getContentResolver().takePersistableUriPermission(dirUri, 
        Intent.FLAG_GRANT_READ_URI_PERMISSION);
2. 通過Uri讀寫文件
  • 創建文件

    // 在mUri目錄(‘DuoKan’目錄)下創建'test.txt'文件
    private void createFile() {
        DocumentFile documentFile = DocumentFile.fromTreeUri(this, mUri);
        DocumentFile file = documentFile.createFile("text/plain", "test.txt");
        if (file != null && file.exists()) {
            LogUtil.log(file.getName() + " created");
        }
    }
    

    主要用到DocumentFile類,和File類的方法類似,有isFile、isDirectory、exists、listFiles等方法

  • 刪除文件

    //刪除"test.txt"
    private void deleteFile() {
        DocumentFile documentFile = DocumentFile.fromTreeUri(this, mUri);
        // listFiles(),列出所有的子文件和文件夾
        for (DocumentFile file : documentFile.listFiles()) {
            if (file.isFile() && "test.txt".equals(file.getName())) {
                boolean delete = file.delete();
                LogUtil.log("deleteFile: " + delete);
                break;
            }
        }
    }
    
  • 寫入數據

    private void writeFile(Uri uri) {
        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
            //這種方法會覆蓋原來文件內容
            OutputStreamWriter output = 
                    new OutputStreamWriter(new FileOutputStream(pfd.getFileDescriptor()));
            // 不能傳uri.toString(),否則FileNotFoundException
            // OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream(uri.toString(), true));
            output.write("這是一段文件寫入測試\n");
            output.close();
            LogUtil.log("寫入成功。");
        } catch (IOException e) {
            LogUtil.log(e);
        }
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章