Android 8.0 targetsdkversion升级到26填坑
目錄
前言
1、動態權限管理
2、ContentResolver
3、FileProvider(File URI)
4、DownloadManager(ContentResolver.openFileDescriptor)
5、后臺service
6、集合API變更
7、通知Notification
8、隱式廣播
9、懸浮窗
前言
近期因為應用市場要求,需要將targetsdkversion升級到26
之前博客中我們了解過targetsdkversion的重要性,當時我們建議輕易不要改動這個參數。
但是這次因為應用市場的硬性要求,我們必須做升級,那么就需要面對升級后帶來的兼容性問題。
1、動態權限管理
最明顯的問題就是權限管理,在6.0加入的動態權限需要我們手動進行處理。這個就老生常談了,這里不展開說了。
2、ContentResolver
處理完權限我們運行程序后,發現app竟然crash了,報錯:
java.lang.SecurityException: Failed to find provider?xxx?for user 0; expected to find a valid ContentProvider for this authority
調查發現我們使用了
getContentResolver().registerContentObserver(uri, false, observer)或?
getContentResolver().notifyChange(uri, observer)查詢相關文檔得知8.0對ContentResolver加了一層安全機制,防止外部訪問app內部使用的數據。
那么怎么解決這個問題?
首先我們需要自定義一個ContentProvider,如果僅僅是為了通知,可以不實現抽象函數
然后在AndroidManifest中
最后在使用時uri需要是content://<authorities>/...形式,如
getContentResolver().registerContentObserver(new URI().parse("content://xxxx/tablename"), false, observer) getContentResolver().notifyChange(new URI().parse("content://xxxx/tablename"), null)3、FileProvider(File URI)
在8.0(實際是7.0)下,當app對外傳遞file URI的時候會導致一個FileUriExposedException。
比如說在app拉起安裝apk,之前代碼是:
Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(Uri.parse(file), "application/vnd.android.package-archive"); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i);當tagsdkversion升級到26就會出現問題。這是因為7.0添加了一項安全機制,app不再允許對外暴露File URI(file://URI),而用Content URI(content://URI)來代替,Content URI會授予URI臨時訪問權限,提高文件訪問的安全性。
那么如何使用Content URI?
添加FileProvider即可,它是ContentProvider的一個子類。
首先,在res的xml目錄(沒有則新建)下,新增一個文件file_provider_paths.xml:
<?xml version="1.0" encoding="utf-8"?> <paths><external-pathname="apkDownload"path="download"/> </paths>這里注意paths一定要小寫,大寫也不會報錯,但是會造成一些麻煩;再有path不能為空。
這里就要詳細解釋一下:
- name是Content URI對外暴露的偽路徑,它對應這path
- path則是真實路徑
- external-path則表示sd卡,這里幾種選擇,如下:
所以在使用要格外注意,一定要與真實地址對應上。比如真實地址為/data/data/<package-name>/cache/apk/1.apk
那么就是:
<cache-pathname="apkDownload"path="apk"/>而最終得到的Content URI則是content://<authorities>/apkDownload/1.apk
可以看到使用authorities(在后面)和name隱藏了真實路徑,這樣就防止對外暴露了路徑
其次,在AndroidManifest中添加:
<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="<packageName>.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_provider_paths" /> </provider>exported和grantUriPermissions都不能缺,否則會引起錯誤
這里的authorities就是最后Content URI中的,而且后面還會使用到
然后,我需要重寫拉起安裝的代碼,如下:
Intent i = new Intent(Intent.ACTION_VIEW); Uri contentUri; if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);contentUri = FileProvider.getUriForFile(context, authorities, file); } else{contentUri = Uri.parse(file); } i.setDataAndType(contentUri, "application/vnd.android.package-archive"); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i);注意這里的authorities一定要與AndroidManifest中的保持一致。
最后再記錄一下開發過程中遇到的幾個問題:
(1)InstallStart: Requesting uid 10087 needs to declare permission android.permission.REQUEST_INSTALL_PACKAGES
? ?在8.0以上,需要在AndroidManifest中添加<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>權限
(2)安裝包解析失敗
???這里有兩種情況,可以根據日志分析出來:
? ?(1)路徑錯誤:日志中有No such file or directory這樣的字眼。檢查文件路徑和上面的配置是否有誤,比如文件在sd卡而xml中是應用cache中。
? ?(2)權限問題,日志如下:
? ? ? ?W/System.err: java.lang.SecurityException: Permission Denial: opening provider ... from ProcessRecord ... that is not exported from .?
? ? ? ?W/PackageInstaller: InstallStaging:Error staging apk from content URIPermission Denial: opening provider?... from ProcessRecord?... that is not exported from?...
? ? ? ? ? ? ?網上對有不少說法:不能使用sd卡,必須用應用空間;還有將android:exported設為true。事實證明都不對,尤其android:exported設為true會造成java.lang.SecurityException:?Provider?must?not?be?exported錯誤。
? ? ? ? ? ? ?這個問題實際上是沒有給intent添加Intent.FLAG_GRANT_READ_URI_PERMISSION這個flag。尤其要注意,因為這里要添加兩個flag,一定要使用addFlags。如果使用setFlags,由于Intent.FLAG_ACTIVITY_NEW_TASK在后面設置,會丟失前面的flag,會導致上面的問題。
(3)FileProvider沖突
當lib或module中的AndroidManifest添加了FileProvider,而主項目也需要添加時,就會出現沖突,gradle編譯錯誤如下:
Error:C:***AndroidManifest.xml:352:13-62 Error:
Attribute provider#android.support.v4.content.FileProvider@authorities value=(***.fileProvider) from AndroidManifest.xml:352:13-62
is also present at [xxxx:xxx] AndroidManifest.xml:19:13-64 value=(***.fileprovider).
Suggestion: add 'tools:replace="android:authorities"' to <provider> element at AndroidManifest.xml:350:9-358:20 to override.
錯誤上建議我們添加tools:replace="android:authorities"來解決問題,實際上我發現這并不能很好的解決問題。那么怎么辦?
簡單的方法是我們自定義一個類,繼承FileProvider,在AndroidManifest中使用這個自定義類,這樣就可以避免沖突了。如:
<providerandroid:name=".MyFileProvider"android:authorities="<packageName>.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_provider_paths" /> </provider>4、DownloadManager(ContentResolver.openFileDescriptor)
同樣在7.0上,為了安全起見對DownloadManager也做了修改,拋棄了COLUMN_LOCAL_FILENAME字段。
在之前我們使用DownloadManager,會使用一個receiver來接收下載結束并處理后續,代碼如下:
@Override public void onReceive(Context context, Intent intent) {DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);if (downloadId == mId) {DownloadManager.Query query = new DownloadManager.Query();query.setFilterById(mId);Cursor cursor = downloadManager.query(query);if (cursor.moveToFirst()) {int urlId = cursor.getColumnIndex(DownloadManager.COLUMN_URI);int stateId = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);String url = cursor.getString(urlId);int state = cursor.getInt(stateId);int pathId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);tmp = cursor.getString(pathId);...可以看到從cursor里可以得到下載狀態,url及下載地址等信息。
但是當targetsdkversion升級到7.0或以上后,在7.0及以上機器上就會crash,報錯如下:
java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
如上所說,為了安全起見拋棄了COLUMN_LOCAL_FILENAME(還有COLUMN_LOCAL_URI字段),讓我們用ContentResolver.openFileDescriptor()代替。
那么ContentResolver.openFileDescriptor()又是什么?
簡單來說會得到一個ParcelFileDescriptor對象,并可以進一步得到一個FileDescriptor對象。
我們可以通過它們打開文件流,比如:
(使用FileDescriptor還有其它方法,都是通過流來處理)
那么我們如果只需要下載文件的完整路徑即可,這時就不需要openFileDescriptor,只需要將上面獲取下載文件的兩行代碼替換為:
在7.0之上處理一下就可以了。
5、后臺service
這里有兩點需要注意:
(1)當app在后臺時,startService啟動一個background service將不再允許,會導致crash。bindService不受影響。
解決方法是避免app不在前端時啟動服務;或者如果一定需要app在后臺啟動服務,請啟動一個foreground?service,但是需要一個常駐的notifacation; 或者使用bindService來啟動服務。具體做法有很多文章,這里就不詳細寫了。
(2)service的存活差異
經測試發現,targetsdkversion的改變對service(background service)的存活也是有影響的。
這塊我有一篇詳細的文章來講解,請見《探討8.0版本下后臺service存活機制及?;睢?/p>
對于targetsdkversion 26的app在8.0及以上版本,想要長時間存活,最好的方式就是使用foreground?service;或者將service綁定到application上bindService;另外一個解決方案就是請求加入耗電白名單,但是這個對用戶不友好。
6、集合API變更
在android8.0上AbstractCollection.removeAll(null)和AbstractCollection.retainAll(null)會引發NullPointerException;之前版本則不會。所以我們在使用這兩個函數前要確保參數不是null,必要是需要判空。
可以看看這兩個函數的代碼:
public boolean removeAll(Collection<?> c) {Objects.requireNonNull(c);boolean modified = false;Iterator<?> it = iterator();while (it.hasNext()) {if (c.contains(it.next())) {it.remove();modified = true;}}return modified; } public boolean retainAll(Collection<?> c) {Objects.requireNonNull(c);boolean modified = false;Iterator<E> it = iterator();while (it.hasNext()) {if (!c.contains(it.next())) {it.remove();modified = true;}}return modified; }可以看到都在首行調用了Objects.requireNonNull(c),這個代碼是:
public static <T> T requireNonNull(T obj) {if (obj == null)throw new NullPointerException();return obj; }可以看到如果是null就會拋出一個空指針錯誤。
7、通知Notification
在Android8.0上,通知做了較大的改動,增加了分組和渠道機制,這樣更加方便了用戶對通知的管理。
那么我們app中的notification相關代碼就需要變動,否則可以無法發出通知。
我們需要為notification增加分組和渠道,如下:
String groupId =?"group1";NotificationChannelGroup group =?new?NotificationChannelGroup(groupId,?"");notificationManager.createNotificationChannelGroup(group);String channelId =?"channel1";NotificationChannel channel =?new?NotificationChannel(channelId,"推廣信息", NotificationManager.IMPORTANCE_DEFAULT);channel.setDescription("推廣信息");channel.setGroup(groupId);notificationManager.createNotificationChannel(channel);NotificationCompat.Builder builder =new NotificationCompat.Builder(context, channelId); //上面使用support包中的NotificationCompat,但是版本需要是26及以上 //或者不使用support包中的類,直接使用Notification.Builder,但是要進行版本判斷//Notification.Builder builder; //if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // ? ?builder = new Notification.Builder(BaseApp.getAppContext(), App.CHANNEL_ID); //} //else{ // ? ?builder = new Notification.Builder(BaseApp.getAppContext()); //}... notificationManager(notificationId, builder.build())這樣通知就能正常發出并展示了,但是其實group并不是必須的,所以可以只設置channel,如:
String channelId =?"channel1";NotificationChannel adChannel =?new?NotificationChannel(channelId,"推廣信息", NotificationManager.IMPORTANCE_DEFAULT);adChannel.setDescription("推廣信息");notificationManager.createNotificationChannel(adChannel);NotificationCompat.Builder builder =new NotificationCompat.Builder(context, channelId); ... notificationManager(notificationId, builder.build())
8、隱式廣播
android8.0之后靜態注冊(manifest中)的隱式廣播將不再起作用,但是有一些隱式廣播除外,靜態注冊它們仍然可以接收到廣播。
解決方法是將隱式廣播改成動態注冊。
靜態注冊的顯式廣播不受影響
9、懸浮窗
使用SYSTEM_ALERT_WINDOW 權限的應用無法再使用以下窗口類型來在其他應用和系統窗口上方顯示提醒窗口:
??? ?TYPE_PHONE
??? ?TYPE_PRIORITY_PHONE
??? ?TYPE_SYSTEM_ALERT
??? ?TYPE_SYSTEM_OVERLAY
??? ?TYPE_SYSTEM_ERROR
??? 相反,應用必須使用名為 TYPE_APPLICATION_OVERLAY 的新窗口類型。
所以我們要在代碼中判斷版本:
if (Build.VERSION.SDK_INT>=26) {windowParams.type= WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; }else{windowParams.type= WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; }
而且在manifest添加權限后,如果在6.0及以上系統中還需要動態請求相關權限:
總結
以上是生活随笔為你收集整理的Android 8.0 targetsdkversion升级到26填坑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android中几种定位方式详解
- 下一篇: Android中的拍照camera和ca