Android版本適配需要注意的坑

一、Android 8.0 適配 --?targetSdkVersion 升級成26 需要注意的一些坑

1. MODE_WORLD_READABLE 模式(表示當前文件可以被其他應用讀取)? 廢棄 (ContentProvider、BroadcastReceiver、Service、SharedPreferences)

Caused by: java.lang.SecurityException: MODE_WORLD_READABLE no longer supported


Android api 24后,對于文件權限進行了限制。 類似蘋果的沙盒模式,應用創(chuàng)建的文件夾,其他應用無權限訪問

所以如果原代碼中有使用MODE_WORLD_READABLE 模式的要替換成?MODE_PRIVATE?模式

2. 獲取通話記錄

Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.CallLogProvider from ProcessRecord{8c75d80 31624:com.ct.client/u0a122} (pid=31624, uid=10122) requires android.permission.READ_CALL_LOG or android.permission.WRITE_CALL_LOG

針對android.permission.READ_CALL_LOG?or?android.permission.WRITE_CALL_LOG做權限適配

3、圖片選擇和裁剪(通過FileProvider實現(xiàn)應用間共享文件)

Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.ct.client/files/com.ct.client/camere/1547090088847.jpg exposed beyond app through ClipData.Item.getUri()??

在AndroidManifest.xml清單文件中注冊 provider

<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.xx.xxx.fileProvider" android:grantUriPermissions="true" android:exported="false">

<!--元數據-->

<meta-data android:name="android.support.FILE_PROVIDER_PATH" android:resource="@xml/file_paths" />

</provider>

需要注意一下幾點:

exported:必須為false

grantUriPermissions:true,表示授予 URI 臨時訪問權限。

authorities 組件標識,都以包名開頭,避免和其它應用發(fā)生沖突。

第二步:

指定共享文件的目錄,需要在res文件夾中新建xml目錄,并且創(chuàng)建file_paths

<resources xmlns:android="http://schemas.android.com/apk/res/android">

<paths>

<external-path path="" name="download"/>

</paths>

</resources>

path=”“,是有特殊意義的,它代表根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個文件了。

name="", 指對應目錄下的對應的文件

第三步:

使用FileProvider

根據版本號把Uri改成使用FiliProvider創(chuàng)建的Uri,

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

cameraFileUri = FileProvider.getUriForFile(mContext, "com.xx.xxx.fileProvider[authorities 對應的值]", new File(saveCamerePath, saveCameraFileName));

} else {

cameraFileUri = Uri.fromFile(new File(saveCamerePath, saveCameraFileName));

}

添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)來對目標應用臨時授權該Uri所代表的文件

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //添加這一句表示對目標應用臨時授權該Uri所代表的文件 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); }

在設置裁剪要保存的?intent.putExtra(MediaStore.EXTRA_OUTPUT, outUri);的時候,這個outUri是要使用Uri.fromFile(file)生成的,而不是使用FileProvider.getUriForFile。

FileProvider所支持的幾種path類型

從Android官方文檔上可以看出FileProvider提供以下幾種path類型:

<files-path path="" name="camera_photos" />

該方式提供在應用的內部存儲區(qū)的文件/子目錄的文件。它對應Context.getFilesDir返回的路徑:eg:"/data/data/com.xx.xxx/files"。

<cache-path name="name" path="path" />

該方式提供在應用的內部存儲區(qū)的緩存子目錄的文件。它對應getCacheDir返回的路徑:eg:“/data/data/com.xx.xxx/cache”;

<external-path name="name" path="path" />

該方式提供在外部存儲區(qū)域根目錄下的文件。它對應Environment.getExternalStorageDirectory返回的路徑:eg:"/storage/emulated/0";

<external-files-path name="name" path="path" />

該方式提供在應用的外部存儲區(qū)根目錄的下的文件。它對應Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)返回的路徑。eg:"/storage/emulated/0/Android/data/com.xx.xxx/files"。

<external-cache-path name="name" path="path" />

該方式提供在應用的外部緩存區(qū)根目錄的文件。它對應Context.getExternalCacheDir()返回的路徑。eg:"/storage/emulated/0/Android/data/com.xx.xxx/cache"


4. 獲取以content開頭的文件拿不到正確路徑

java.lang.IllegalArgumentException: column '_data' does not exist

拿到uri之后進行版本判斷大于等于24(即Android7.0)用最新的獲取路徑方式

Stringstr ="";

if(Build.VERSION.SDK_INT >=24) {? ?

? ? str = getFilePathFromURI(this, uri);//新的方式

}else{? ?

? ? ?str = getPath(this, uri);你自己之前的獲取方法

}

public String getFilePathFromURI(Context context, Uri contentUri) {

File rootDataDir = context.getFilesDir();

String fileName =getFileName(contentUri);

if (!TextUtils.isEmpty(fileName)) {

File copyFile =new File(rootDataDir + File.separator + fileName);

copyFile(context, contentUri, copyFile);

return copyFile.getAbsolutePath();

}

return null;

}

public static String getFileName(Uri uri) {

if (uri ==null)return null;

String fileName =null;

String path = uri.getPath();

int cut = path.lastIndexOf('/');

if (cut != -1) {

fileName = path.substring(cut +1);

}

return fileName;

}

public void copyFile(Context context, Uri srcUri, File dstFile) {

try {

InputStream inputStream = context.getContentResolver().openInputStream(srcUri);

if (inputStream ==null)return;

OutputStream outputStream =new FileOutputStream(dstFile);

copyStream(inputStream, outputStream);

inputStream.close();

outputStream.close();

}catch (Exception e) {

e.printStackTrace();

}

}

public int copyStream(InputStream input, OutputStream output)throws Exception, IOException {

final int BUFFER_SIZE =1024 *2;

byte[] buffer =new byte[BUFFER_SIZE];

BufferedInputStream in =new BufferedInputStream(input, BUFFER_SIZE);

BufferedOutputStream out =new BufferedOutputStream(output, BUFFER_SIZE);

int count =0, n =0;

try {

while ((n = in.read(buffer,0, BUFFER_SIZE)) != -1) {

out.write(buffer,0, n);

count += n;

}

out.flush();

}finally {

try {

out.close();

}catch (IOException e) {

}

try {

in.close();

}catch (IOException e) {

}

}

return count;

}

5、 7.0的手機安裝沒問題,但是在8.0上安裝,app沒有反應,一閃而過

增加新權限

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

Intent intent = new Intent(Intent.ACTION_VIEW)

改為

Intent intent = new Intent (Intent.ACTION_INSTALL_PACKAGE);


6. 解析包安裝失敗。

安裝時把intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 這句話放在 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)前面。

File apkfile : 這個是你的apk文件地址對象。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

? ? ? ? ? ? Uri apkUri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", apkfile);? // 這個地方 關鍵

? ? ? ? ? ? Intent install = new Intent(Intent.ACTION_VIEW);

? ? ? ? ? ? install.setDataAndType(apkUri, "application/vnd.android.package-archive");

? ? ? ? ? ? install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

? ? ? ? ? ? install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//添加這一句表示對目標應用臨時授權該Uri所代表的文件

? ? ? ? ? ? mContext.startActivity(install);

? ? ? ? } else {

? ? ? ? ? ? Intent i = new Intent(Intent.ACTION_VIEW);

? ? ? ? ? ? i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

? ? ? ? ? ? i.setDataAndType(Uri.parse("file://" + apkfile.toString()),

? ? ? ? ? ? ? ? ? ? "application/vnd.android.package-archive");

? ? ? ? ? ? mContext.startActivity(i);

? ? ? ? }

7. 通知欄不顯示

在Application中創(chuàng)建渠道

@Override

protected void onCreate() {

super.onCreate();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

String channelId ="chat";

String channelName ="聊天消息";

int importance = NotificationManager.IMPORTANCE_HIGH;

createNotificationChannel(channelId, channelName, importance);

channelId ="subscribe";

channelName ="訂閱消息";

importance = NotificationManager.IMPORTANCE_DEFAULT;

createNotificationChannel(channelId, channelName, importance);

}

}

@TargetApi(Build.VERSION_CODES.O)

private void createNotificationChannel(String channelId, String channelName,int importance) {

NotificationChannel channel =new NotificationChannel(channelId, channelName, importance);

NotificationManager notificationManager = (NotificationManager) getSystemService(

NOTIFICATION_SERVICE);

notificationManager.createNotificationChannel(channel);

}

根據渠道發(fā)送消息new NotificationCompat.Builder(this, channelName)

public void sendChatMsg(View view) {

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

Notification notification =new NotificationCompat.Builder(this,"chat")

.setContentTitle("收到一條聊天消息")

.setContentText("聊天消息具體內容")

.setWhen(System.currentTimeMillis())

.setSmallIcon(R.drawable.icon)

.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))

.setAutoCancel(true)

.build();

manager.notify(1, notification);

}

public void sendSubscribeMsg(View view) {

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

Notification notification =new NotificationCompat.Builder(this,"subscribe")

.setContentTitle("收到一條訂閱消息")

.setContentText("訂閱消息 具體內容")

.setWhen(System.currentTimeMillis())

.setSmallIcon(R.drawable.icon)

.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon))

.setAutoCancel(true)

.build();

manager.notify(2, notification);

}

}


8. 懸浮窗適配

使用 SYSTEM_ALERT_WINDOW 權限的應用無法再使用以下窗口類型來在其他應用和系統(tǒng)窗口上方顯示提醒窗口:

TYPE_PHONE

TYPE_PRIORITY_PHONE

TYPE_SYSTEM_ALERT

TYPE_SYSTEM_OVERLAY

TYPE_SYSTEM_ERROR

相反,應用必須使用名為 TYPE_APPLICATION_OVERLAY 的新窗口類型。

也就是說需要在之前的基礎上判斷一下:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {? ?

mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

}? else{? ?

mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

}

需要新增權限

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

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

9. BroadcastReceiver無法接收廣播

在Android 8之后需要setComponent才能收到廣播消息,其中,ComponentName接收兩個參數,參數1是自定義廣播的包名,參數2是自定義廣播的路徑。

Intent intent = new Intent(action);

intent.putExtra(INTENT_DATA_PUSH, data);intent.addCategory(context.getPackageName());

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {

intent.setComponent(new ComponentName(context.getPackageName(), com.xx.broadcastReceiver);

}

context.sendBroadcast(intent);

二、Android 版本 9適配?--?targetSdkVersion 升級成28 需要注意的一些坑


1. 限制 HTTP 網絡請求(*)

Android 9.0 中限制了 HTTP(明文傳輸)網絡請求,若仍繼續(xù)使用HTTP請求,則會在日志中提示以下異常(只是無法正常發(fā)出請求,不會導致應用崩潰):

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

適配的方法如下:

第一種

在資源目錄中新建一個 xml 文件作為網絡安全配置文件,例如 xml/network_security_config.xml,然后在文件中填寫以下內容:

<?xml version="1.0" encoding="utf-8"?>

<network-security-config>

?<base-config cleartextTrafficPermitted="true">

<trust-anchors>

<certificates src="system" overridePins="true" />

<certificates src="user" overridePins="true" />?

??</trust-anchors>

?</base-config>?

</network-security-config>

在AndroidManifest.xml進行配置:

<application? ??

...? ? android:networkSecurityConfig="@xml/network_security_config">? ? ...

</application>

第二種

Android 6.0 中引入了是否允許網絡使用明文傳輸的配置:

<application android:usesCleartextTraffic=["true" | "false"]>

原來默認為 true,但在 Android 9.0 中默認值改為了 false,因此將配置手動設為 true 即可解決明文傳輸被限制的問題

2. 棄用 Apache HTTP Client

由于官方在 Android 9.0 中移除了所有 Apache HTTP Client 相關的類,因此我們的應用或是一些第三方庫如果使用了這些類,就會拋出找不到類的異常:

java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/conn/scheme/SchemeRegistry;

若需要繼續(xù)使用 Apache HTTP Client ,可通過以下方法進行適配:

在 AndroidManifest.xml 中添加以下內容:

<uses-library android:name="org.apache.http.legacy" android:required="false"/>

或者在應用中直接將 Apache HTTP Client 相關的類打包并進行引用

3. 限制非 SDK 接口的調用(*)

3.1 簡述

一直以來,官方提供的接口分為了 SDK 接口和非 SDK 接口。SDK 接口即官方支持的接口,開發(fā)者可以直接調用不會有任何限制。一般而言,SDK 接口都記錄在官方的接口索引中,沒有記錄的就視為非 SDK 接口,例如一些使用了@hide標注的方法。

以往開發(fā)者對于非 SDK 接口的調用通常是利用反射或者JNI間接調用的方式進行,但這樣的調用方式如果處理不當會比較容易出現(xiàn)一些未知的錯誤。為了提升用戶體驗和降低應用發(fā)生崩潰的風險,Android 9.0 對應用能使用的非 SDK 接口實施了限制,具體的限制手段請見下表:


此外,為了開發(fā)者能夠順利過渡到 Android 9.0,官方對非 SDK 接口進行了分類,共分為三類,light-greylist(淺灰名單)、dark-greylist(深灰名單)以及blacklist(黑名單):

light-greylist(淺灰名單):對于此名單中的非 SDK 接口,官方暫未找到可替代的 SDK 接口,因此開發(fā)者仍可繼續(xù)訪問(如果 targetSdkVersion 大于等于28時會出現(xiàn)警告)。

dark-greylist(深灰名單):targetSdkVersion 小于28時仍可繼續(xù)使用此名單中的接口,但會出現(xiàn)警告提示;大于等于28時,這些接口將會限制訪問。

blacklist(黑名單):無論 targetSdkVersion 為多少,只要應用運行在 Android 9.0 平臺上,訪問此名單中的接口都會受限

3.2 如何測試應用是否使用非 SDK 接口

可以通過以下方式進行測試(詳情請至官方文檔):

使用 Android 9.0 或更高版本的設備調試應用

使用 StrictMode API 進行測試

使用 veridex 工具對應用進行掃描

建議使用第三種方式,該工具的掃描結果會列出應用對于三個限制名單中的接口的調用細節(jié)。

4. 前臺服務權限

在 Android 9.0 中,應用在使用前臺服務之前必須先申請FOREGROUND_SERVICE權限,否則就會拋出 SecurityException 異常。

此外,由于FOREGROUND_SERVICE權限只是普通權限,因此開發(fā)者只需在 AndroidManifest.xml 中注冊此權限即可,系統(tǒng)會自動對此權限進行授權:

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


5. 強制執(zhí)行 FLAG_ACTIVITY_NEW_TASK 要求(*)

在 Android 7.0(API 級別 24)之前,若開發(fā)者需要通過非 Activity context 啟動 Activity,就必須設置 Intent 標志FLAG_ACTIVITY_NEW_TASK,否則會啟動失敗并拋出以下異常

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity? context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

但這個要求在更新 Android 7.0 以后由于系統(tǒng)問題被臨時取消了,開發(fā)者即使不設置標志也可以正常啟動 Activity。而在 Android 9.0 中官方修復了這個問題,這個要求重新開始強制執(zhí)行,因此開發(fā)者在適配 Android 9.0 時需要注意這個問題。

6. 不允許共享 WebView 數據目錄

Android 9.0 中為了改善應用穩(wěn)定性和數據完整性,應用無法再讓多個進程共用同一 WebView 數據目錄。此類數據目錄一般存儲 Cookie、HTTP 緩存以及其他與網絡瀏覽有關的持久性和臨時性存儲。

如果開發(fā)者需要在多進程中使用 WebView,則必須先調用WebView.setDataDirectorySuffix()方法為每個進程設置用于存儲 WebView 數據的目錄。若多進程 WebView 之間需要共享數據,開發(fā)者需自己通過 IPC 的方式實現(xiàn)。

此外,若開發(fā)者只想在一個進程中使用 WebView,并且希望嚴格執(zhí)行這個規(guī)則,可以通過在其他進程中調用WebView.disableWebView()方法,這樣其他進程創(chuàng)建 WebView 實例就會拋出異常

7. 其他 API 方面的修改

7.1 Region.Op 相關

Android 9.0 中如果在使用繪圖裁剪功能時設置了除Region.Op.INTERSECT或Region.Op.DIFFERENCE以外的類型,就會拋出以下異常:

java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed

具體原因是官方廢棄了那幾個具有Region.Op參數的裁剪方法,如clipRect(@NonNull RectF rect, @NonNull Region.Op op):

@Deprecated

public boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op) {

checkValidClipOp(op);

return nClipRect(mNativeCanvasWrapper, rect.left, rect.top, rect.right, rect.bottom,

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");

}

}

對于這個問題,可以通過以下方法進行適配:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

? ? canvas.clipPath(path);

} else {

? ? canvas.clipPath(path, Region.Op.XOR);// REPLACE、UNION 等類型

}

7.2 Build.SERIAL 被棄用(*)

Android 9.0 之前,開發(fā)者可以使用Build.SERIAL獲取設備的序列號。現(xiàn)在這個方法被棄用了,Build.SERIAL將始終設置為 "UNKNOWN" 以保護用戶的隱私。

適配的方法為先請求READ_PHONE_STATE權限,然后調用Build.getSerial()方法。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容