開源中國(guó)客戶端 Android 10 經(jīng)驗(yàn)適配指南,含代碼

轉(zhuǎn)載 https://my.oschina.net/huanghaibin/blog/3106432

我們App的適配從 targetSdkVersion = 26跨版本升級(jí)到29,因此會(huì)遇到大量的坑,最終的版本配置如下:

image

現(xiàn)在進(jìn)入填坑適配指南,包含實(shí)際經(jīng)驗(yàn)代碼,絕不照搬翻譯文檔

1.Region.Op相關(guān)異常:java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed

當(dāng) targetSdkVersion >= Build.VERSION_CODES.P 時(shí)調(diào)用 canvas.clipPath(path, Region.Op.XXX); 引起的異常,參考源碼如下:

@Deprecated
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op) {
     checkValidClipOp(op);
     return nClipPath(mNativeCanvasWrapper, path.readOnlyNI(), op.nativeInt);
}

private static void checkValidClipOp(@NonNull Region.Op op) {
     if (sCompatiblityVersion >= Build.VERSION_CODES.P
         && op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) {
         throw new IllegalArgumentException(
                    "Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed");
     }
}

我們可以看到當(dāng)目標(biāo)版本從Android P開始,Canvas.clipPath(@NonNull Path path, @NonNull Region.Op op) ; 已經(jīng)被廢棄,而且是包含異常風(fēng)險(xiǎn)的廢棄API,只有 Region.Op.INTERSECT 和 Region.Op.DIFFERENCE 得到兼容,目前不清楚google此舉目的如何,僅僅如此簡(jiǎn)單就拋出異常提示開發(fā)者適配,幾乎所有的博客解決方案都是如下簡(jiǎn)單粗暴:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    canvas.clipPath(path);
} else {
    canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等
}

但我們一定需要一些高級(jí)邏輯運(yùn)算效果怎么辦?如小說的仿真翻頁(yè)閱讀效果,解決方案如下,用Path.op代替,先運(yùn)算Path,再給canvas.clipPath:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
    Path mPathXOR = new Path();
    mPathXOR.moveTo(0,0);
    mPathXOR.lineTo(getWidth(),0);
    mPathXOR.lineTo(getWidth(),getHeight());
    mPathXOR.lineTo(0,getHeight());
    mPathXOR.close();
    //以上根據(jù)實(shí)際的Canvas或View的大小,畫出相同大小的Path即可
    mPathXOR.op(mPath0, Path.Op.XOR);
    canvas.clipPath(mPathXOR);
}else {
    canvas.clipPath(mPath0, Region.Op.XOR);
}

2.明文HTTP限制

當(dāng) targetSdkVersion >= Build.VERSION_CODES.P 時(shí),默認(rèn)限制了HTTP請(qǐng)求,并出現(xiàn)相關(guān)日志:

java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy

第一種解決方案:在AndroidManifest.xml中Application添加如下節(jié)點(diǎn)代碼

<application android:usesCleartextTraffic="true">

第二種解決方案:在res目錄新建xml目錄,已建的跳過 在xml目錄新建一個(gè)xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下節(jié)點(diǎn)代碼

android:networkSecurityConfig="@xml/network_config"

名字隨機(jī),內(nèi)容如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

3.Android Q(10)中的媒體資源讀寫

相關(guān)的Android Q 行為變更不做細(xì)說,網(wǎng)上大部分博客關(guān)于Android Q 適配都在說行為變更,我們將根據(jù)實(shí)際遇到的問題,實(shí)際解決

1、掃描系統(tǒng)相冊(cè)、視頻等,圖片、視頻選擇器都是通過ContentResolver來提供,主要代碼如下:

private static final String[] IMAGE_PROJECTION = {
            MediaStore.Images.Media.DATA,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.BUCKET_ID,
            MediaStore.Images.Media.BUCKET_DISPLAY_NAME};

 Cursor imageCursor = mContext.getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    IMAGE_PROJECTION, null, null, IMAGE_PROJECTION[4] + " DESC");

String path = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[0]));
String name = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[1]));
int id = imageCursor.getInt(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[2]));
String folderPath = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[3]));
String folderName = imageCursor.getString(imageCursor.getColumnIndexOrThrow(IMAGE_PROJECTION[4]));

//Android Q 公有目錄只能通過Content Uri + id的方式訪問,以前的File路徑全部無效,如果是Video,記得換成MediaStore.Videos
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
      path  = MediaStore.Images.Media
                       .EXTERNAL_CONTENT_URI
                       .buildUpon()
                       .appendPath(String.valueOf(id)).build().toString();
 }

2、判斷公有目錄文件是否存在,自Android Q開始,公有目錄File API都失效,不能直接通過new File(path).exists();判斷公有目錄文件是否存在,正確方式如下:

public static boolean isAndroidQFileExists(Context context, String path){
        if (context == null) {
            return false;
        }
        AssetFileDescriptor afd = null;
        ContentResolver cr = context.getContentResolver();
        try {
            Uri uri = Uri.parse(path);
            afd = cr.openAssetFileDescriptor(Uri.parse(path), "r");
            if (afd == null) {
                return false;
            } else {
                close(afd);
            }
        } catch (FileNotFoundException e) {
            return false;
        }finally {
            close(afd);
        }
        return true;
}

3、保存或者下載文件到公有目錄,保存Bitmap同理,如Download,MIME_TYPE類型可以自行參考對(duì)應(yīng)的文件類型,這里只對(duì)APK作出說明

public static void copyToDownloadAndroidQ(Context context, String sourcePath, String fileName, String saveDirName){
        ContentValues values = new ContentValues();
        values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
        values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
        values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/" + saveDirName.replaceAll("/","") + "/");

        Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = context.getContentResolver();

        Uri insertUri = resolver.insert(external, values);
        if(insertUri == null) {
            return;
        }

        String mFilePath = insertUri.toString();

        InputStream is = null;
        OutputStream os = null;
        try {
            os = resolver.openOutputStream(insertUri);
            if(os == null){
                return;
            }
            int read;
            File sourceFile = new File(sourcePath);
            if (sourceFile.exists()) { // 文件存在時(shí)
                is = new FileInputStream(sourceFile); // 讀入原文件
                byte[] buffer = new byte[1444];
                while ((read = is.read(buffer)) != -1) {
                    os.write(buffer, 0, read);
                }
                is.close();
                os.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            close(is,os);
        }

}

4、保存圖片相關(guān)

 /**
     * 通過MediaStore保存,兼容AndroidQ,保存成功自動(dòng)添加到相冊(cè)數(shù)據(jù)庫(kù),無需再發(fā)送廣告告訴系統(tǒng)插入相冊(cè)
     *
     * @param context      context
     * @param sourceFile   源文件
     * @param saveFileName 保存的文件名
     * @param saveDirName  picture子目錄
     * @return 成功或者失敗
     */
    public static boolean saveImageWithAndroidQ(Context context,
                                                  File sourceFile,
                                                  String saveFileName,
                                                  String saveDirName) {
        String extension = BitmapUtil.getExtension(sourceFile.getAbsolutePath());

        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
        values.put(MediaStore.Images.Media.DISPLAY_NAME, saveFileName);
        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/" + saveDirName);

        Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = context.getContentResolver();

        Uri insertUri = resolver.insert(external, values);
        BufferedInputStream inputStream = null;
        OutputStream os = null;
        boolean result = false;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
            if (insertUri != null) {
                os = resolver.openOutputStream(insertUri);
            }
            if (os != null) {
                byte[] buffer = new byte[1024 * 4];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                os.flush();
            }
            result = true;
        } catch (IOException e) {
            result = false;
        } finally {
            Util.close(os, inputStream);
        }
        return result;
}

4.EditText默認(rèn)不獲取焦點(diǎn),不自動(dòng)彈出鍵盤

該問題出現(xiàn)在 targetSdkVersion >= Build.VERSION_CODES.P 情況下,且設(shè)備版本為Android P以上版本,目前我們沒有從源碼中查到相關(guān)判斷改動(dòng),解決方法在onCreate中加入如下代碼:

mEditText.post(() -> {
       mEditText.requestFocus();
       mEditText.setFocusable(true);
       mEditText.setFocusableInTouchMode(true);
});

5.Only fullscreen activities can request orientation 異常

該問題出現(xiàn)在 targetSdkVersion >= Build.VERSION_CODES.O_MR1 ,也就是 API 27,當(dāng)設(shè)備為Android 26時(shí)(27以上已經(jīng)修復(fù),也許google覺得不妥當(dāng),又改回來了),如果非全面屏透明activity固定了方向,則出現(xiàn)該異常,但是當(dāng)我們?cè)谛∶住Ⅶ茸宓華ndroid 26機(jī)型測(cè)試的時(shí)候,并沒有該異常,華為機(jī)型則報(bào)該異常,這是何等的臥槽。。。沒辦法,去掉透明style或者去掉固定方向代碼即可,其它無解

6.安裝APK Intent及其它文件相關(guān)Intent

/*
* 自Android N開始,是通過FileProvider共享相關(guān)文件,但是Android Q對(duì)公有目錄 File API進(jìn)行了限制
* 從代碼上看,又變得和以前低版本一樣了,只是必須加上權(quán)限代碼Intent.FLAG_GRANT_READ_URI_PERMISSION
*/ 
private void installApk() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
            //適配Android Q,注意mFilePath是通過ContentResolver得到的,上述有相關(guān)代碼
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.parse(mFilePath) ,"application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            startActivity(intent);
            return ;
        }

        File file = new File(saveFileName + "osc.apk");
        if (!file.exists())
            return;
        Intent intent = new Intent(Intent.ACTION_VIEW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), "net.oschina.app.provider", file);
            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
        } else {
            intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        startActivity(intent);
}

我們APK開發(fā)實(shí)踐中暫時(shí)遇到的坑就這些,當(dāng)然Android Q的改動(dòng)是相當(dāng)大的,例如還有App私有沙箱文件、定位權(quán)限和后臺(tái)彈出Activity限制,這些都必須根據(jù)自身實(shí)踐去踩坑適配,有條件的盡可能去閱讀官方文檔,參考改進(jìn)。

?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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