Android 8.0 以釋出很長時間了,基于使用者裝置和市場要求等遲遲沒有适配更新;如今适配時遇到一些問題,整理記錄一下! 官網 對 Android 8.0 新特性以及适配相關的介紹非常清楚,小菜根據官方要求逐漸進行适配更新;
更新 SDK
首先要處理的是 targetSDK = 26 compileSDK >= 26,同步之後要注意項目中三方 SDK 是否需要更新适配,若需要更新請認真參考三方 SDK 文檔進行更新,之後小菜建議分别對 debug 和 release 進行測試是否可以進行正常打包;這一步可能簡單到隻需更改一個版本号而已,也可能牽連很多複雜的三方包的更新等,需認真對待;
targetSdkVersion = 26
推送通知
小菜在更新 SDK 之後測試推送消息,部分消息無法正常展示,原因在于 Android 8.0 添加了通知管道,允許建立不同的通知管道類型,我們可以建立不同類型的通知管道,使用者方可以選擇性的關閉不感興趣或低優先級的推送消息,優勢是不會因為推送消息過多而整體取消;
建立通知管道需要 ChannelID / ChannelName / importance 三個參數,ChannelID 為唯一的,ChannelName 會展示在系統設定說明中,importance 為通知重要程度;
- IMPORTANCE_HIGH 可在任何地方顯示,有聲音
- IMPORTANCE_DEFAULT 可任何地方顯示,有聲音但不會在視覺上幹擾
- IMPORTANCE_MIN 無聲音,隻出現在狀态欄中,不能與 startForeground 一起用
// Android8.0 将各個通知放入特定管道中
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(PUSH_CHANNEL_ID, PUSH_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
// 需要傳入 ChannelID
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, PUSH_CHANNEL_ID);
...
notificationBuilder.build();
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicWZwpmLjZjM5QmMjZTOlFjMwMWMxQjMzEDO2YDZhRWOhhjNwEDN0cTNkZTNy8CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.jpeg)
小菜的項目中推送相對簡單,官網提供了很多豐富的樣式和細節,需要的請官網查閱;
Apk 下載下傳安裝
自從 Android N 開始,Android 便加大了對檔案管理的限制;Android 7.0 之後棄用了 Uri.fromFile 方式采用 FileProvider 方式處理本地檔案路徑;
Uri.fromFile 方式檔案路徑:
file:/storage/emulated/0/Android/data/包名/files/downloaded_app.apk
FileProvider 方式檔案路徑:
content:/storage/emulated/0/Android/data/包名/files/downloaded_app.apk
- 在 AndroidManifest.xml 中聲明 Provider;
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
- 在 res 下建立 xml 檔案夾,之後建立與 Provider 中對應的 file_path 檔案;
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="external_files_path"
path="Download" />
</paths>
- 使用隐式意圖安裝 Apk;
private void installApk(File apk) {
Uri uri = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
uri = Uri.fromFile(apk);
} else {
uri = FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", apk);
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
mContext.startActivity(intent);
}
- Android O 的新權限要求;
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
注意事項
- android:name 預設為 android.support.v4.content.FileProvider 但有時需要內建三方 SDK 時已經占用,使用新的 Provider 時可以建立 Provider 防止沖突;
public class MyFileProvider extends FileProvider {}
- android:authorities 一般為 Uri 域名,具有唯一性,與安裝 Apk 時路徑相同;
android:authorities="${applicationId}.fileprovider"
FileProvider.getUriForFile(mContext, mContext.getPackageName() + ".fileprovider", apk);
- android:exported="false":FileProvider 預設為私有的不可導出;
- android:grantUriPermissions="true":允許擷取檔案的臨時通路權限;
- android:resource="@xml/file_path": 設定 FileProvider 通路的檔案路徑,可自由定義;
- file_path.xml 中為檔案路徑,可自由設定,Android 預設提供了如下幾種;
【files-path】 -> "/data/data/包名/files"
【external-path】 -> "/storage/emulated/0"
【cache-path】 -> "/data/data/包名/cache"
【external-files-path】 -> "/storage/emulated/0/Android/data/包名/files"
【external-cache-path】 -> "/storage/emulated/0/Android/data/包名/cache"
背景廣播限制
Android 8.0 之後,系統對背景操作增加了更多的限制,App 應用無法使用 AndroidManifest 中注冊大部分隐式廣播;尤其是 "android.net.conn.CONNECTIVITY_CHANGE",但對于
如下廣播例外;
// 開機廣播
Intent.ACTION_LOCKED_BOOT_COMPLETED
Intent.ACTION_BOOT_COMPLETED
// 使用者增删
Intent.ACTION_USER_INITIALIZE
// 時區廣播
Intent.ACTION_TIMEZONE_CHANGED
// 語言區域
Intent.ACTION_LOCALE_CHANGED
// USB
UsbManager.ACTION_USB_ACCESSORY_ATTACHED
UsbManager.ACTION_USB_ACCESSORY_DETACHED
UsbManager.ACTION_USB_DEVICE_ATTACHED
UsbManager.ACTION_USB_DEVICE_DETACHED
// 藍牙
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED
BluetoothDevice.ACTION_ACL_CONNECTED
BluetoothDevice.ACTION_ACL_DISCONNECTED
// 電話
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED
TelephonyIntents.ACTION_*_SUBSCRIPTION_CHANGED
TelephonyIntents.SECRET_CODE_ACTION
TelephonyManager.ACTION_PHONE_STATE_CHANGED
TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED
TelecomManager.ACTION_PHONE_ACCOUNT_UNREGISTERED
// 登入賬号
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION
// 資料清除
Intent.ACTION_PACKAGE_DATA_CLEARED
适配方式則是把不能使用的隐式廣播動态注冊;注意:要在同一個上下文環境下【注冊和銷毀】;
TestReceiver testReceiver;
public void initTestReceiver() {
testReceiver = new TestReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.intent.action.NEW_OUTGOING_CALL");
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
context.registerReceiver(testReceiver, intentFilter);
}
public void destroyTestReceiver() {
if (testReceiver != null) {
context.unregisterReceiver(testReceiver);
}
}
背景服務限制
Android 8.0 對于服務的限制也增強了,不能直接用 startService 啟動背景服務;
方案一:
由背景服務轉為前台服務,根據版本判斷,使用 startForegroundService(),但是應用必須在建立服務後的五秒内調用該服務的 startForeground();若不調用,日志會提示沒有調用 startForeground,甚至會出現 ANR 應用崩潰;
// 啟動 Service
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(new Intent(MainActivity.this, TestService.class));
} else {
startService(new Intent(MainActivity.this, TestService.class));
}
public class TestService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
initNotification();
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
initNotification();
return super.onStartCommand(intent, flags, startId);
}
private void initNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager =
(NotificationManager) getSystemService(android.content.Context.NOTIFICATION_SERVICE);
NotificationChannel channel =
new NotificationChannel("push", "push_name", NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
Notification notification =
new NotificationCompat.Builder(this, "push").setContentTitle("ACE_DEMO").setContentText("前台服務").build();
startForeground(1, notification);
}
}
}
服務轉為前台需要手動開啟一個 Notification,對于部分 Android O 裝置,若沒有建立 Channel 或 Notification 内容不填充預設在切到背景時,系統會提示通知 XX在背景消耗電量;若 Notification 正常同樣會提示背景進行,隻是内容為填充内容;小菜嘗試了很多方式均不能正常取消通知,是以官網推薦了方案二來啟動背景服務;
方案二:
官方提供了另一種解決方案
JobScheduler可以用計劃作業替代背景服務,即使用 JobService 替代 Service;JobService 是從 Android 5.0 之後引入的,小菜為了适配相容性,低版本依舊采用普通的 Service 高版本采用 JobService;
JobService 中通過 onStartJob 處理業務邏輯,通過 onStopJob 結束作業;調用是借助 JobInfo.Builder 構造器來啟動;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class TestJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
// do something
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}
public static void startTestService(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
JobInfo.Builder builder = new JobInfo.Builder(Constants.JOB_ALARM_SERVICE_ID, new ComponentName(context, TestJobService.class));
builder.setOverrideDeadline(5000);
scheduler.schedule(builder.build());
} else {
context.startService(new Intent(context, TestJobService.class));
}
}
方案三:
為了簡便的使用 JobService 和 Service,小菜嘗試了三方的
android-job,無需區分版本,最低支援到 API 14,基本滿足日常版本;
Job 中通過 onRunJob 處理業務邏輯,通過 JobRequest.Builder 構造器來調用;且 Job 提供了包括立即啟動/延遲啟動/循環啟動等多種方式,詳細方法請參照官網;
public class TestJobCreator implements JobCreator {
@Override
@Nullable
public Job create(@NonNull String tag) {
switch (tag) {
case TestSyncJob.TAG:
return new TestSyncJob();
default:
return null;
}
}
}
public class TestSyncJob extends Job {
public static final String TAG = "job_test_tag";
@Override
@NonNull
protected Result onRunJob(Params params) {
// run your job here
return Result.SUCCESS;
}
public static void scheduleJob() {
new JobRequest.Builder(TestSyncJob.TAG)
.setExecutionWindow(30_000L, 40_000L)
.build()
.schedule();
}
}
JobManager.create(this).addJobCreator(new TestJobCreator());
Android 8.0 的适配還包括 藍牙/背景定位 等限制,小菜在實際中并未應用,詳細内容請查閱官方文檔;小菜僅記錄一下實際适配中遇到的問題難點;若有錯誤請多多指導!
來源:阿策小和尚