Android 8.0(Oreo) 適配
前陣子,市場中心丟來一個鍋,說華為、360、應用寶要求開發者適配 Android P,否則應用將被不推薦、隱藏甚至下架(華為),從 2018 年 8 月 1 日起,所有向 Google Play 首次提交的新應用都必須針對 Android 8.0 (API 等級 26) 開發; 2018 年 11 月 1 日起,所有 Google Play 的現有應用更新同樣必須針對 Android 8.0。嚇得我趕緊做了下適配,原本覺得應該不難,沒想到過程是曲折的,前途終究還是光明的。
適配的第一步,修改targetSdkVersion為26或以上,然後針對Oreo新的行為變更進行適配。
1. 自適應啟動圖示(非必要)
之前的啟動圖示都是mipmap中的靜態圖片ic_launcher。到後來7.1的時候谷歌開始推廣圓形圖示,在原來android:icon的基礎上又添加了android:roundIcon屬性來讓你的app支援圓形圖示。
到了8.0,情況又變了,如右圖:多了一個mipmap-anydpi-v26資料夾,裡面也是啟動圖,但是不是一張圖片,而是xml檔案。
<?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <background android:drawable="@mipmap/ic_launcher_background"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/> </adaptive-icon>
該檔案中主要是設定兩張圖片,一個前景色一個背景色。
其實這個還是按照之前的方式處理,並不會出現什麼特別的問題,主要是在Android原生的ROM桌面圖示顯示有問題,圖示會變得特別大或者被一個白色的圓包裹著。
2. 通知欄
Android 8.0 引入了通知渠道,其允許您為要顯示的每種通知型別建立使用者可自定義的渠道。使用者介面將通知渠道稱之為通知類別。
針對 8.0 的應用,建立通知前需要建立渠道,建立通知時需要傳入 channelId,否則通知將不會顯示。示例程式碼如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //建立通知渠道 @SuppressLint("WrongConstant") NotificationChannel mChannel = new NotificationChannel(channelId, "通知渠道名稱", NotificationManager.IMPORTANCE_DEFAULT); mChannel.setDescription("渠道描述");//渠道描述 mChannel.enableLights(false);//是否顯示通知指示燈 mChannel.enableVibration(false);//是否振動 mChannel.setImportance(NotificationManager.IMPORTANCE_HIGH);//通知級別 NotificationManager notificationManager = (NotificationManager) context.getSystemService( NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(mChannel);//建立通知渠道 NotificationCompat.Builder builder = new NotificationCompat.Builder(this,channelId); }
channelId對應一類渠道通知,mChannel.setImportance()可以設定通知的重要性。
- IMPORTANCE_MIN 開啟通知,不會彈出,但沒有提示音,狀態列中無顯示
- IMPORTANCE_LOW 開啟通知,不會彈出,不發出提示音,狀態列中顯示
- IMPORTANCE_DEFAULT 開啟通知,不會彈出,發出提示音,狀態列中顯示
- IMPORTANCE_HIGH 開啟通知,會彈出,發出提示音,狀態列中顯示
3.後臺執行限制
(1)如果針對 Android 8.0 的應用嘗試在不允許其建立後臺服務的情況下使用 startService() 函式,則該函式將引發一個 IllegalStateException。目前我在實際專案中並沒有看到這個Exception的出現,不過為了避免出鍋,我們還是try-catch一下比較靠譜。
try {
context.startService(intent);
} catch (Throwable th) {
DebugLog.i("service", "start service: " + intent.getComponent() + "error: " + th);
ExceptionUtils.printExceptionTrace(th);
}
(2)靜態廣播
- 針對 Android 8.0的應用無法繼續在其清單中為隱式廣播註冊廣播接收器
- 應用可以繼續在它們的清單中註冊顯式廣播
- 應用可以在執行時使用Context.registerReceiver()為任意廣播(不管是隱式還是顯式)註冊接收器
- 需要簽名許可權的廣播不受此限制所限,因為這些廣播只會傳送到使用相同證書籤名的應用,而不是傳送到裝置上的所有應用
很多人的部落格說,8.0只能在程式碼中註冊傳送,不能在manifest檔案中註冊了,其實不然。在manifest中我們依舊可以註冊,不過在傳送的時候我們需要特殊處理下:
Intent intent = new Intent();
intent.setAction(action);
intent.setComponent(new ComponentName(context.getPackageName(),"receiver的包路徑"));
context.sendBroadcast(intent);
靜態註冊的時候我們傳送需要新增component,讓廣播知道傳送到哪裡。不過最好還是在程式碼中動態註冊,註冊了要記得取消註冊以免造成記憶體洩漏。
4. 允許安裝未知來源應用
針對 8.0 的應用需要在 AndroidManifest.xml 中宣告 REQUEST_INSTALL_PACKAGES 許可權,否則將無法進行應用內升級。
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
5.許可權
8.0之前你申請讀外部儲存的許可權READ_EXTERNAL_STORAGE,你會自動被賦予寫外部儲存的許可權WRITE_EXTERNAL_STORAGE,因為他們屬於同一組(android.permission-group.STORAGE)許可權,但是現在8.0不一樣了,讀就是讀,寫就是寫,不能混為一談。不過你授予了讀之後,雖然下次還是要申請寫,但是在申請的時候,申請會直接通過,不會讓使用者再授權一次了。
額外篇:Android7.0適配之許可權更改
由於之前也沒有適配7.0的許可權,所以順帶說下7.0適配的問題。
對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含檔案 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。對於這種跳轉到第三方應用的URI需要使用FileProvider進行處理。
String cachePath = getApplicationContext().getExternalCacheDir().getPath();
File picFile = new File(cachePath, "test.jpg");
Uri picUri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, 100);
這是常見的開啟系統相機拍照的程式碼,拍照成功後,照片會儲存在picFile檔案中。
這段程式碼在Android 7.0之前是沒有任何問題,但是如果你嘗試在7.0的系統上執行,會丟擲FileUriExposedException異常。
使用FileProvider
FileProvider使用大概分為以下幾個步驟:
1.manifest中申明FileProvider,android:authorities一般設定為包名+fileProvider。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="xxx.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
2.res/xml中定義對外暴露的資料夾路徑,即android:resource="@xml/file_paths"
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="/storage/emulated/0/" name="files_root" />
<external-path path="." name="external_storage_root" />
</paths>
在paths節點內部支援以下幾個子節點,分別為:
<root-path/> 代表裝置的根目錄new File("/");
<files-path/> 代表context.getFilesDir()
<cache-path/> 代表context.getCacheDir()
<external-path/> 代表Environment.getExternalStorageDirectory()
<external-files-path>代表context.getExternalFilesDirs()
<external-cache-path>代表getExternalCacheDirs()
每個節點都支援兩個屬性:
name
path
path即為代表目錄下的子目錄,比如:
<external-path
name="external"
path="pics" />
代表的目錄即為:Environment.getExternalStorageDirectory()/pics,其他同理。
當這麼宣告以後,程式碼可以使用你所宣告的當前檔案
3.生成content://型別的Uri
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri
// 相容Android 7.0版本
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
outputFileUri = FileProvider.getUriForFile(mContext,BuildConfig.APPLICATION_ID
+ ".fileProvider",newFile);
}else {
outputFileUri = Uri.fromFile(newFile);
}
4.給Uri授予臨時許可權
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
5.使用Intent傳遞Uri
File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(),
"com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
// 授予目錄臨時共享許可權
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 100);
許可權變更影響到的功能有:
1.拍照;
new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
2.使用第三方應用開啟檔案或者連結;
new Intent("android.intent.action.VIEW");
3.apk安裝
/**
* 安裝apk
* @param filePath
*/
public static void installAPK(Context context,String filePath) {
try {
boolean isRight = UtilZipCheck.isErrorZip(filePath);
if (isRight) {
Intent intent = new Intent(Intent.ACTION_VIEW);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID
+ ".fileProvider", new File(filePath));
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive");
}
context.startActivity(intent);
}
} catch (Exception exception) {
exception.printStackTrace();
}
}