天天看點

Android 8.0 簡單适配那些事兒

      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 為通知重要程度;

  1. IMPORTANCE_HIGH 可在任何地方顯示,有聲音
  2. IMPORTANCE_DEFAULT 可任何地方顯示,有聲音但不會在視覺上幹擾
  3. 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();           
Android 8.0 簡單适配那些事兒

      小菜的項目中推送相對簡單,官網提供了很多豐富的樣式和細節,需要的請官網查閱;

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           
  1. 在 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>           
  1. 在 res 下建立 xml 檔案夾,之後建立與 Provider 中對應的 file_path 檔案;
<?xml version="1.0" encoding="utf-8"?>
<paths>
  <external-files-path
      name="external_files_path"
      path="Download" />
</paths>           
  1. 使用隐式意圖安裝 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);
}           
  1. Android O 的新權限要求;
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>           

注意事項

  1. android:name 預設為 android.support.v4.content.FileProvider 但有時需要內建三方 SDK 時已經占用,使用新的 Provider 時可以建立 Provider 防止沖突;
public class MyFileProvider extends FileProvider {}           
  1. android:authorities 一般為 Uri 域名,具有唯一性,與安裝 Apk 時路徑相同;
android:authorities="${applicationId}.fileprovider"

FileProvider.getUriForFile(mContext, mContext.getPackageName() +  ".fileprovider", apk);           
  1. android:exported="false":FileProvider 預設為私有的不可導出;
  2. android:grantUriPermissions="true":允許擷取檔案的臨時通路權限;
  3. android:resource="@xml/file_path": 設定 FileProvider 通路的檔案路徑,可自由定義;
  4. 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 正常同樣會提示背景進行,隻是内容為填充内容;小菜嘗試了很多方式均不能正常取消通知,是以官網推薦了方案二來啟動背景服務;

Android 8.0 簡單适配那些事兒

方案二:

      官方提供了另一種解決方案

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 的适配還包括 藍牙/背景定位 等限制,小菜在實際中并未應用,詳細内容請查閱官方文檔;小菜僅記錄一下實際适配中遇到的問題難點;若有錯誤請多多指導!

來源:阿策小和尚