1. 程式人生 > >Android targetSdkVersion 從22提到25 你需要知道的一切

Android targetSdkVersion 從22提到25 你需要知道的一切

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

Android 6.0

執行時許可權

相機,相簿,下載,語音,定位….
此版本引入了一種新的許可權模式,如今,使用者可直接在執行時管理應用許可權。這種模式讓使用者能夠更好地瞭解和控制權限,同時為應用開發者精簡了安裝和自動更新過程。使用者可為所安裝的各個應用分別授予或撤銷許可權。
對於以 Android 6.0(API 級別 23)或更高版本為目標平臺的應用,請務必在執行時檢查和請求許可權。要確定您的應用是否已被授予許可權,請呼叫新增的 checkSelfPermission()方法。要請求許可權,請呼叫新增的requestPermissions()

方法。即使您的應用並不以 Android 6.0(API 級別 23)為目標平臺,您也應該在新許可權模式下測試您的應用。如需瞭解有關在您的應用中支援新許可權模式的詳情,請參閱使用系統許可權。如需瞭解有關如何評估新模式對應用的影響的提示,請參閱許可權最佳做法
許可權管理工具類

    package cn.loveshow.live.util;

    import android.Manifest;
    import android.app.Activity;
    import android.content.Context;
    import android.content.pm.PackageManager;
    import
android.os.Build; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; import java.util.ArrayList; import java.util.Arrays; /** * Created by Fungo_Xiaoke on 2017/5/4 14:18. * eamil:
[email protected]
* 許可權工具類 */
public class PermissionUtils { /** * @param context 上下文 * @param activity activity * @param permissions 許可權陣列 * @param requestCode 申請碼 * @return true 有許可權 false 無許可權 */ public static boolean checkAndApplyfPermissionActivity(Activity activity, String[] permissions, int requestCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { permissions = checkPermissions(activity, permissions); if (permissions != null && permissions.length > 0) { ActivityCompat.requestPermissions(activity, permissions, requestCode); return false; } else { return true; } } else { return true; } } /** * @param context 上下文 * @param mFragment fragment * @param permissions 許可權陣列 * @param requestCode 申請碼 * @return true 有許可權 false 無許可權 */ public static boolean checkAndApplyfPermissionFragment( Fragment mFragment, String[] permissions, int requestCode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { permissions = checkPermissions(mFragment.getActivity(), permissions); if (permissions != null && permissions.length > 0) { if (mFragment.getActivity() != null) { mFragment.requestPermissions(permissions, requestCode); } return false; } else { return true; } } else { return true; } } /** * @param context 上下文 * @param permissions 許可權陣列 * @return 還需要申請的許可權 */ private static String[] checkPermissions(Context context, String[] permissions) { if (permissions == null || permissions.length == 0) { return new String[0]; } ArrayList<String> permissionLists = new ArrayList<>(); permissionLists.addAll(Arrays.asList(permissions)); for (int i = permissionLists.size() - 1; i >= 0; i--) { if (ContextCompat.checkSelfPermission(context, permissionLists.get(i)) == PackageManager.PERMISSION_GRANTED) { permissionLists.remove(i); } } String[] temps = new String[permissionLists.size()]; for (int i = 0; i < permissionLists.size(); i++) { temps[i] = permissionLists.get(i); } return temps; } /** * 檢查申請的許可權是否全部允許 */ public static boolean checkPermission(int[] grantResults) { if (grantResults == null || grantResults.length == 0) { return true; } else { int temp = 0; for (int i : grantResults) { if (i == PackageManager.PERMISSION_GRANTED) { temp++; } } return temp == grantResults.length; } } /** * 沒有獲取到許可權的提示 * * @param permissions 許可權名字陣列 */ public static void showPermissionsToast(Activity activity, @NonNull String[] permissions) { if (permissions.length > 0) { for (String permission : permissions) { showPermissionToast(activity, permission); } } } /** * 沒有獲取到許可權的提示 * * @param permission 許可權名字 */ private static void showPermissionToast(Activity activity, @NonNull String permission) { if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { //使用者勾選了不再詢問,提示使用者手動開啟許可權 switch (permission) { case Manifest.permission.CAMERA: ToastUtils.showShort("相機許可權已被禁止,請在應用管理中開啟許可權"); break; case Manifest.permission.WRITE_EXTERNAL_STORAGE: ToastUtils.showShort("檔案許可權已被禁止,請在應用管理中開啟許可權"); break; case Manifest.permission.RECORD_AUDIO: ToastUtils.showShort("錄製音訊許可權已被禁止,請在應用管理中開啟許可權"); break; case Manifest.permission.ACCESS_FINE_LOCATION: ToastUtils.showShort("位置許可權已被禁止,請在應用管理中開啟許可權"); break; } } else { //使用者沒有勾選了不再詢問,拒絕了許可權申請 switch (permission) { case Manifest.permission.CAMERA: ToastUtils.showShort("沒有相機許可權"); break; case Manifest.permission.WRITE_EXTERNAL_STORAGE: ToastUtils.showShort("沒有檔案讀取許可權"); break; case Manifest.permission.RECORD_AUDIO: ToastUtils.showShort("沒有錄製音訊許可權"); break; case Manifest.permission.ACCESS_FINE_LOCATION: ToastUtils.showShort("沒有位置許可權"); break; } } } }

用法

       if (PermissionUtils.checkAndApplyfPermissionActivity(this,
            new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 
            REQUESRCARMEA)) {
           //獲取到許可權的操作  沒有許可權會申請許可權  然後在onRequestPermissionsResult處理申請的結果
       }

       @Override
       public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
           super.onRequestPermissionsResult(requestCode, permissions, grantResults);
           if (PermissionUtils.checkPermission(grantResults)) {
               //申請許可權成功
                switch (requestCode) {
                     case 0x001:
                         //dosomething
                         break;
                     case 0x002:
                        //dosomething
                         break;
                     ...
                }
           } else {
               //提示沒有什麼許可權
               PermissionUtils.showPermissionsToast(activity, permissions);
                //or 去許可權管理介面
                //gotoPermissionManager(mContext);
           }
       }

沒有許可權去許可權管理介面

    /**
     * 去應用許可權管理介面
     */
    public static void gotoPermissionManager(Context context) {
        Intent intent;
        ComponentName comp;
        //防止刷機出現的問題
        try {
            switch (Build.MANUFACTURER) {
                case "Huawei":
                    intent = new Intent();
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
                    intent.setComponent(comp);
                    context.startActivity(intent);
                    break;
                case "Meizu":
                    intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
                    intent.addCategory(Intent.CATEGORY_DEFAULT);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    context.startActivity(intent);
                    break;
                case "Xiaomi":
                    String rom = getSystemProperty("ro.miui.ui.version.name");
                    if ("v5".equals(rom)) {
                        Uri packageURI = Uri.parse("package:" + context.getApplicationInfo().packageName);
                        intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
                    } else {//if ("v6".equals(rom) || "v7".equals(rom)) {
                        intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
                        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
                        intent.putExtra("extra_pkgname", context.getPackageName());
                    }
                    context.startActivity(intent);
                    break;
                case "Sony":
                    intent = new Intent();
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity");
                    intent.setComponent(comp);
                    context.startActivity(intent);
                    break;
                case "OPPO":
                    intent = new Intent();
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity");
                    intent.setComponent(comp);
                    context.startActivity(intent);
                    break;
                case "LG":
                    intent = new Intent("android.intent.action.MAIN");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
                    intent.setComponent(comp);
                    context.startActivity(intent);
                    break;
                case "Letv":
                    intent = new Intent();
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    intent.putExtra("packageName", BuildConfig.APPLICATION_ID);
                    comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps");
                    intent.setComponent(comp);
                    context.startActivity(intent);
                    break;
                default:
                    getAppDetailSettingIntent(context);
                    break;
            }
        } catch (Exception e) {
            getAppDetailSettingIntent(context);
        }
    }

    /**
     * 獲取系統屬性值
     */
    public static String getSystemProperty(String propName) {
        String line;
        BufferedReader input = null;
        try {
            Process p = Runtime.getRuntime().exec("getprop " + propName);
            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
            line = input.readLine();
            input.close();
        } catch (IOException ex) {
            Log.e(TAG, "Unable to read sysprop " + propName, ex);
            return null;
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    Log.e(TAG, "Exception while closing InputStream", e);
                }
            }
        }
        return line;
    }

      //以下程式碼可以跳轉到應用詳情,可以通過應用詳情跳轉到許可權介面(6.0系統測試可用)
     public static void getAppDetailSettingIntent(Context context) {
        Intent localIntent = new Intent();
        localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (Build.VERSION.SDK_INT >= 9) {
            localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
            localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
        } else if (Build.VERSION.SDK_INT <= 8) {
            localIntent.setAction(Intent.ACTION_VIEW);
            localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
            localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
        }
        launchApp(context, localIntent);
    }

      /**
     * 安全的啟動APP
     */
    public static boolean launchApp(Context ctx, Intent intent) {
        if (ctx == null)
            throw new NullPointerException("ctx is null");
        try {
            ctx.startActivity(intent);
            return true;
        } catch (ActivityNotFoundException e) {
            Logger.e(e);
            return false;
        }
    }

取消支援 Apache HTTP 客戶端
Android 6.0 版移除了對 Apache HTTP 客戶端的支援。如果您的應用使用該客戶端,並以 Android 2.3(API 級別 9)或更高版本為目標平臺,請改用HttpURLConnection 類。此 API 效率更高,因為它可以通過透明壓縮和響應快取減少網路使用,並可最大限度降低耗電量。要繼續使用 Apache HTTP API,您必須先在 build.gradle 檔案中宣告以下編譯時依賴項:

  android {
      useLibrary 'org.apache.http.legacy'
  }

BoringSSL
Android 正在從使用 OpenSSL 庫轉向使用 BoringSSL 庫。如果您要在應用中使用 Android NDK,請勿連結到並非 NDK API 組成部分的加密庫,如libcrypto.so和 libssl.so。這些庫並非公共 API,可能會在不同版本和裝置上毫無徵兆地發生變化或出現故障。此外,您還可能讓自己暴露在安全漏洞的風險之下。請改為修改原生程式碼,以通過 JNI 呼叫 Java 加密 API,或靜態連結到您選擇的加密庫。

bugly錯誤

bugly錯誤

通知

此版本移除了 Notification.setLatestEventInfo()方法。請改用 Notification.Builder 類來構建通知。要重複更新通知,請重複使用Notification.Builder 例項。呼叫 build() 方法可獲取更新後的 Notification 例項。
adb shell dumpsys notification 命令不再列印輸出您的通知文字。請改用 adb shell dumpsys notification –noredact 命令列印輸出 notification 物件中的文字。build()方法在4.1以上(16+)的系統才能用。

notification

例子

gif

Android 金鑰庫變更
從此版本開始,Android 金鑰庫提供程式不再支援 DSA。但仍支援 ECDSA。
停用或重置安全鎖定螢幕時(例如,由使用者或裝置管理員執行此類操作時),系統將不再刪除需要閒時加密的金鑰,但在上述事件期間會刪除需要閒時加密的金鑰。

APK 驗證
該平臺現在執行的 APK 驗證更為嚴格。如果在清單中宣告的檔案在 APK 中並不存在,該 APK 將被視為已損壞。移除任何內容後必須重新簽署 APK。

Android7.0

系統許可權更改
為了提高私有檔案的安全性,面向 Android 7.0 或更高版本的應用私有目錄被限制訪問 (0700)。此設定可防止私有檔案的元資料洩漏,如它們的大小或存在性。此許可權更改有多重副作用:
* 私有檔案的檔案許可權不應再由所有者放寬,為使用 MODE_WORLD_READABLE 和/或 MODE_WORLD_WRITEABLE 而進行的此類嘗試將觸發 SecurityException
:迄今為止,這種限制尚不能完全執行。應用仍可能使用原生 API 或 File API 來修改它們的私有目錄許可權。但是,我們強烈反對放寬私有目錄的許可權。

在應用間共享檔案
對於面向 Android 7.0 的應用,Android 框架執行的 StrictModeAPI 政策禁止在您的應用外部公開 file://URI。如果一項包含檔案 URI 的 intent 離開您的應用,則應用出現故障,並現 FileUriExposedException。異常。要在應用間共享檔案,您應傳送一項 content://URI,並授予 URI 臨時訪問許可權。進行此授權的最簡單方式是使用FileProvider類。如需瞭解有關許可權和共享檔案的詳細資訊,請參閱共享檔案

FileProvider用法
AndroidManiFest.xml新增

  <application>
  ...
    <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/provider_paths"/>
    </provider>
  ...
  </application>

配置applicationId

res目錄下新建xml資料夾,建立provider_paths.xml檔案

  <?xml version="1.0" encoding="utf-8"?>
  <resources>
    <paths>
    <!--  前面兩個是bugly的 -->
    <!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
    <external-path
        name="beta_external_path"
        path="Download/" />
    <!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
    <external-path
        name="beta_external_files_path"
        path="Android/data/" />

    <external-path
        name="sdcard_files"
        path="" />
    <!--相機相簿裁剪-->
    <external-files-path
        name="camera_has_sdcard"
        path="" />
    <files-path
        name="camera_no_sdcard"
        path="" />
    </paths>
    <!--<paths>-->
    <!-- xml檔案是唯一設定分享的目錄 ,不能用程式碼設定-->
     <!--1.<files-path>        getFilesDir()  /data/data//files目錄-->
     <!--2.<cache-path>        getCacheDir()  /data/data//cache目錄-->
     <!--3.<external-path>       Environment.getExternalStorageDirectory()  -->
     <!--4.<external-files-path>    
       Context.getExternalFilesDir(String)  Context.getExternalFilesDir(null)  
       == SDCard/Android/data/你的應用的包名/files/ 目錄-->
     <!--5.<external-cache-path>      Context.getExternalCacheDir().-->

     <!--  path :代表設定的目錄下一級目錄 eg:<external-path path="images/"-->
     <!--整個目錄為Environment.getExternalStorageDirectory()+"/images/"-->
     <!--name: 代表定義在Content中的欄位 eg:name = "myimages" ,並且請求的內容的檔名為default_image.jpg-->
     <!--則 返回一個URI   content://com.example.myapp.fileprovider/myimages/default_image.jpg-->

   <!--</paths>-->
  </resources>

確認下路徑名
路徑
路徑

FileProvider 頭部設定的對應標籤
FileProvider

FileProvider 獲取對應路徑邏輯 解析xml檔案 對比對應的標籤 獲取對應的路徑
FileProvider

修改所有用到Uri的地方 圖中的 BuildConfig.APPLICATION_ID 最好還是改成 context.getPackageName()
Uri修改

APK Signature Scheme v2
Android 7.0引入了全新的 APK Signature Scheme v2。這是加強對包的校驗,啟動了新的簽名後,像美團的多渠道打包方案在7.0機器上就會報錯了。
解決的辦法也很簡單,官方提供了關閉v2簽名的方法,只需要在gradle上配置一下即可:

  signingConfigs {
      release {
      .......
      v2SigningEnabled false
     }
  }

其他

Android6.0

USB 連線
預設情況下,現在通過 USB 埠進行的裝置連線設定為僅充電模式。要通過 USB 連線訪問裝置及其內容,使用者必須明確地為此類互動授予許可權。如果您的應用支援使用者通過 USB 埠與裝置進行互動,請將必須顯式啟用互動考慮在內。

瀏覽器書籤變更
此版本移除了對全域性書籤的支援。android.provider.Browser.getAllBookmarks() 和 android.provider.Browser.saveBookmark() 方法現已移除。同樣,READ_HISTORY_BOOKMARKS 許可權和 WRITE_HISTORY_BOOKMARKS 許可權也已移除。如果您的應用以 Android 6.0(API 級別 23)或更高版本為目標平臺,請勿從全域性提供程式訪問書籤或使用書籤許可權。您的應用應改為在內部儲存書籤資料。

:當執行 Android 6.0(API 級別 23)的裝置發起後臺 WLAN 或藍芽掃描時,在外部裝置看來,該操作的發起來源是一個隨機化 MAC 地址。

WLAN 和網路連線變更
此版本對 WLAN API 和 Networking API 引入了以下行為變更。
* 現在,您的應用只能更改由您建立的 WifiConfiguration 物件的狀態。系統不允許您修改或刪除由使用者或其他應用建立的 WifiConfiguration 物件。
* 在之前的版本中,如果應用利用帶有 disableAllOthers=true
設定的 enableNetwork() 強制裝置連線特定 WLAN 網路,裝置將會斷開與移動資料網路等其他網路的連線。在此版本中,裝置不再斷開與上述其他網路的連線。如果您的應用的 targetSdkVersion
為 “20” 或更低,則會固定連線所選 WLAN 網路。如果您的應用的 targetSdkVersion 為 “21”
或更高,請使用多網路 API(如 openConnection()bindSocket() 和新增的bindProcessToNetwork()
方法)來確保通過所選網路傳送網路流量。

相機服務變更
在此版本中,相機服務中共享資源的訪問模式已從之前的“先到先得”訪問模式更改為高優先順序程序優先的訪問模式。對服務行為的變更包括:
* 根據客戶端應用程序的“優先順序”授予對相機子系統資源的訪問權,包括開啟和配置相機裝置。帶有對使用者可見 Activity 或前臺 Activity 的應用程序一般會被授予較高的優先順序,從而使相機資源的獲取和使用更加可靠;
* 當高優先順序的應用嘗試使用相機時,系統可能會“驅逐”正在使用相機客戶端的低優先順序應用。在已棄用的 Camera API 中,這會導致系統為被驅逐的客戶端呼叫 onError()。在 Camera2 API 中,這會導致系統為被驅逐的客戶端呼叫 onDisconnected()
* 在配備相應相機硬體的裝置上,不同的應用程序可同時獨立開啟和使用不同的相機裝置。但現在,如果在多程序用例中同時訪問相機會造成任何開啟的相機裝置的效能或能力嚴重下降,相機服務會檢測到這種情況並禁止同時訪問。即使並沒有其他應用直接嘗試訪問同一相機裝置,此變更也可能導致低優先順序客戶端被“驅逐”。
* 更改當前使用者會導致之前使用者帳戶擁有的應用內活動相機客戶端被驅逐。對相機的訪問僅限於訪問當前裝置使用者擁有的使用者個人資料。舉例來說,這意味著,當用戶切換到其他帳戶後,“來賓”帳戶實際上無法讓使用相機子系統的程序保持執行狀態。

執行時
ART 執行時環境現在可正確實現 newInstance() 方法的訪問規則。此變更修正了之前版本中 Dalvik 無法正確檢查訪問規則的問題。如果您的應用使用newInstance() 方法,並且您想重寫訪問檢查,請呼叫 setAccessible() 方法(將輸入引數設定為 true)。如果您的應用使用 v7 appcompat 庫v7 recyclerview 庫,則您必須更新應用以使用這些庫的最新版本。否則,請務必更新從 XML 引用的任何自定義類,以便能夠訪問它們的類建構函式。此版本更新了動態連結程式的行為。動態連結程式現在可以識別庫的 soname 與其路徑之間的差異(公開錯誤 6670),並且現在已實現了按 soname 搜尋。之前包含錯誤的 DT_NEEDED 條目(通常是開發計算機檔案系統上的絕對路徑)卻仍工作正常的應用,如今可能會出現載入失敗。現已正確實現 dlopen(3) RTLD_LOCAL 標記。請注意,RTLD_LOCAL 是預設值,因此不顯式使用 RTLD_LOCAL 的 dlopen(3) 呼叫將受到影響(除非您的應用顯式使用 RTLD_GLOBAL)。使用 RTLD_LOCAL 時,在隨後通過呼叫 dlopen(3) 載入的庫中並不能使用這些符號(這與由 DT_NEEDED 條目引用的情況截然不同)。

在之前版本的 Android 上,如果您的應用請求系統載入包含文字重定位資訊的共享庫,系統會顯示警告,但仍允許載入共享庫。從此版本開始,如果您的應用的目標 SDK 版本為 23 或更高,則系統會拒絕載入該庫。為幫助您檢測庫是否載入失敗,您的應用應該記錄 dlopen(3) 失敗日誌,並在日誌中加入dlerror(3) 呼叫返回的問題描述文字。要詳細瞭解如何處理文字重定位,請參閱此指南

低電耗模式和應用待機模式
此版本引入了針對空閒裝置和應用的最新節能優化技術。這些功能會影響所有應用,因此請務必在這些新模式下測試您的應用。
* 低電耗模式:如果使用者拔下裝置的電源插頭,並在螢幕關閉後的一段時間內使其保持不活動狀態,裝置會進入低電耗模式,在該模式下裝置會嘗試讓系統保持休眠狀態。在該模式下,裝置會定期短時間恢復正常工作,以便進行應用同步,還可讓系統執行任何掛起的操作。
* 應用待機模式:應用待機模式允許系統判定應用在使用者未主動使用它時處於空閒狀態。當用戶有一段時間未觸控應用時,系統便會作出此判定。如果拔下了裝置電源插頭,系統會為其視為空閒的應用停用網路訪問以及暫停同步和作業。

文字選擇
現在,當用戶在您的應用中選擇文字時,您可以在一個浮動工具欄中顯示“剪下”“複製”“貼上”等文字選擇操作。其在使用者互動實現上與為單個檢視啟用上下文操作模式中所述的上下文操作欄類似。
要實現可用於文字選擇的浮動工具欄,請在您的現有應用中做出以下更改:
* 在 View 物件或 Activity 物件中,將 ActionMode 呼叫從 startActionMode(Callback)更改為 startActionMode(Callback, ActionMode.TYPE_FLOATING)。
* 改為使用 ActionMode.Callback 的現有實現擴充套件 ActionMode.Callback2
* 替代 onGetContentRect() 方法,用於提供 Rect 內容物件(如文字選擇矩形)在檢視中的座標。
* 如果矩形的定位不再有效,並且這是唯一需要宣告為無效的元素,請調invalidateContentRect() 方法。

Android for Work 變更
此版本包含下列針對 Android for Work 的行為變更:
* **個人上下文中的工作聯絡人。**Google 撥號器通話記錄現在會在使用者檢視通話記錄時顯示工作聯絡人。將 setCrossProfileCallerIdDisabled() 設定為 true 可在 Google 撥號器通話記錄中隱藏託管配置檔案聯絡人。僅當將 setBluetoothContactSharingDisabled() 設定為 false 時,才可以通過藍芽將工作聯絡人隨個人聯絡人一起顯示給裝置。預設情況下,它設定為 true。
* WLAN 配置刪除:現在,當刪除某個託管配置檔案時,將會移除由配置檔案所有者新增的 WLAN 配置(例如,通過呼叫 addNetwork()
方法新增的配置)。
* WLAN 配置鎖定:如果 WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN 不為零,則使用者無法再修改或刪除任何由活動裝置所有者建立的 WLAN 配置。使用者仍可建立和修改其自己的 WLAN 配置。活動裝置所有者擁有編輯或刪除任何 WLAN 配置(包括並非由其建立的配置)的許可權。
* 通過新增 Google 帳戶下載裝置規範控制器:向託管環境以外的裝置新增需要通過裝置規範控制器 (DPC) 應用管理的 Google 帳戶時,帳戶新增流程現在會提示使用者安裝相應的 WPC。在裝置初始設定嚮導中通過 Settings > Accounts 新增帳戶時,也會出現此行為。
*
* 呼叫 setCameraDisabled() 方法只會影響呼叫該方法的使用者的相機;從託管配置檔案呼叫它不會影響主使用者執行的相機應用。
* 此外,setKeyguardDisabledFeatures() 方法現在除了可供裝置所有者使用外,還可供配置檔案所有者使用。
* 配置檔案所有者可設定以下鍵盤鎖限制:
* KEYGUARD_DISABLE_TRUST_AGENTSKEYGUARD_DISABLE_FINGERPRINT,它們影響配置檔案上級使用者的鍵盤鎖設定。
* KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS,它隻影響應用在託管配置檔案中生成的通知。
* DevicePolicyManager.createAndInitializeUser() 方法和 DevicePolicyManager.createUser() 方法已棄用。
* 當給定使用者的應用在前臺執行時,setScreenCaptureDisabled() 方法現在也會遮蔽輔助結構。
* EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM 現在預設為 SHA-256。出於向後相容性考慮,仍然支援 SHA-1,但未來將會取消該支援。
* EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM 現在只接受 SHA-256。
Android 6.0(API 級別 23)中曾經存在的 Device initializer API 現已刪除
EXTRA_PROVISIONING_RESET_PROTECTION_PARAMETERS
已刪除,因此 NFC 佔位配置無法通過程式設計解鎖受恢復出廠設定保護的裝置。
* 您現在可以使用 EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE extra 在對託管裝置進行 NFC 配置期間向裝置所有者應用傳遞資料。
* Android for Work API 針對 M 執行時許可權(包括 Work 配置檔案、輔助層及其他內容)進行了優化。新增的 DevicePolicyManager 許可權 API 不會影響 M 之前版本的應用。
* 當用戶退出通過 ACTION_PROVISION_MANAGED_PROFILEACTION_PROVISION_MANAGED_DEVICE intent 發起的設定流程的同步部分時,系統現在會返回RESULT_CANCELED 結果程式碼。
* 對其他 API 的變更:
* 流量消耗:android.app.usage.NetworkUsageStats 類已重新命名為 NetworkStats
* 對全域性設定的變更:
* 這些設定不再通過 setGlobalSettings() 進行設定:
* BLUETOOTH_ON
* DEVELOPMENT_SETTINGS_ENABLED
* MODE_RINGER
* NETWORK_PREFERENCE
* WIFI_ON
* 這些全域性設定現在可通過 setGlobalSettings() 進行設定:
* WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN

Android7.0

無障礙改進
為提高平臺對於視力不佳或視力受損使用者的易用性,Android 7.0 做出了一些更改。這些更改一般並不要求更改您的應用程式碼,不過您應仔細檢查並使用您的應用測試這些功能,以評估它們對使用者體驗的潛在影響。

電池和記憶體
Android 7.0 包括旨在延長裝置電池壽命和減少 RAM 使用的系統行為變更。這些變更可能會影響您的應用訪問系統資源,以及您的應用通過特定隱式 intent 與其他應用互動的方式。

螢幕縮放
Android 7.0 支援使用者設定顯示尺寸,以放大或縮小螢幕上的所有元素,從而提升裝置對視力不佳使用者的可訪問性。使用者無法將螢幕縮放至低於最小螢幕寬度 sw320dp,該寬度是 Nexus 4 的寬度,也是常規中等大小手機的寬度。
正常效果
執行 Android 7.0 系統映像的裝置增大顯示尺寸後的效果。

當裝置密度發生更改時,系統會以如下方式通知正在執行的應用:
* 如果是面向 API 級別 23 或更低版本系統的應用,系統會自動終止其所有後臺程序。這意味著如果使用者切換離開此類應用,轉而開啟 Settings 螢幕並更改 Display size 設定,則系統會像處理記憶體不足的情況一樣終止該應用。如果應用具有任何前臺程序,則系統會如處理執行時更改中所述將配置變更通知給這些程序,就像對待裝置螢幕方向變更一樣。
* 如果是面向 Android 7.0 的應用,則其所有程序(前臺和後臺)都會收到有關配置變更的通知,如處理執行時更改中所述。

大多數應用並不需要進行任何更改即可支援此功能,不過前提是這些應用遵循 Android 最佳做法。具體要檢查的事項:
* 在螢幕寬度為 sw320dp 的裝置上測試您的應用,並確保其充分執行。
* 當裝置配置發生變更時,更新任何與密度相關的快取資訊,例如快取點陣圖或從網路載入的資源。當應用從暫停狀態恢復執行時,檢查配置變更。
注:如果您要快取與配置相關的資料,則最好也包括相關元資料,例如該資料對應的螢幕尺寸或畫素密度。儲存這些元資料便於您在配置變更後決定是否需要重新整理快取資料。
* 避免用畫素單位指定尺寸,因為畫素不會隨螢幕密度縮放。應改為使用與密度無關畫素 (dp) 單位指定尺寸。

NDK 應用連結至平臺庫
從 Android 7.0 開始,系統將阻止應用動態連結非公開 NDK 庫,這種庫可能會導致您的應用崩潰。此行為變更旨在為跨平臺更新和不同裝置提供統一的應用體驗。即使您的程式碼可能不會連結私有庫,但您的應用中的第三方靜態庫可能會這麼做。因此,所有開發者都應進行相應檢查,確保他們的應用不會在執行 Android 7.0 的裝置上崩潰。如果您的應用使用原生程式碼,則只能使用公開 NDK API

低電耗模式
Android 6.0(API 級別 23)引入了低電耗模式,當用戶裝置未插接電源、處於靜止狀態且螢幕關閉時,該模式會推遲 CPU 和網路活動,從而延長電池壽命。而 Android 7.0 則通過在裝置未插接電源且螢幕關閉狀態下、但不一定要處於靜止狀態(例如使用者外出時把手持式裝置裝在口袋裡)時應用部分 CPU 和網路限制,進一步增強了低電耗模式。
圖 1. 低電耗模式如何應用第一級系統活動限制以延長電池壽命的圖示。
當裝置處於充電狀態且螢幕已關閉一定時間後,裝置會進入低電耗模式並應用第一部分限制:關閉應用網路訪問、推遲作業和同步。如果進入低電耗模式後設備處於靜止狀態達到一定時間,系統則會對PowerManager.WakeLock
AlarmManager 鬧鈴、GPS 和 WLAN 掃描應用餘下的低電耗模式限制。無論是應用部分還是全部低電耗模式限制,系統都會喚醒裝置以提供簡短的維護時間視窗,在此視窗期間,應用程式可以訪問網路並執行任何被推遲的作業/同步。
圖 2. 低電耗模式如何在裝置處於靜止狀態達到一定時間後應用第二級系統活動限制的圖示。
請注意,啟用螢幕或插接裝置電源時,系統將退出低電耗模式並移除這些處理限制。此項新增的行為不會影響有關使您的應用適應 Android 6.0(API 級別 23)中所推出的舊版本低電耗模式的建議和最佳做法,如對低電耗模式和應用待機模式進行鍼對性優化中所討論。您仍應遵循這些建議(例如使用 Google 雲訊息傳遞 (GCM) 傳送和接收訊息)並開始安排更新計劃以適應新增的低電耗模式行為。

Project Svelte:後臺優化
Android 7.0 移除了三項隱式廣播,以幫助優化記憶體使用和電量消耗。此項變更很有必要,因為隱式廣播會在後臺頻繁啟動已註冊偵聽這些廣播的應用。刪除這些廣播可以顯著提升裝置效能和使用者體驗。
移動裝置會經歷頻繁的連線變更,例如在 WLAN 和移動資料之間切換時。目前,可以通過在應用清單中註冊一個接收器來偵聽隱式 CONNECTIVITY_ACTION
廣播,讓應用能夠監控這些變更。由於很多應用會註冊接收此廣播,因此單次網路切換即會導致所有應用被喚醒並同時處理此廣播。
同理,在之前版本的 Android 中,應用可以註冊接收來自其他應用(例如相機)的隱式 ACTION_NEW_PICTUREACTION_NEW_VIDEO 廣播。當用戶使用相機應用拍攝照片時,這些應用即會被喚醒以處理廣播。

為緩解這些問題,Android 7.0 應用了以下優化措施:
* 面向 Android 7.0 開發的應用不會收到 CONNECTIVITY_ACTION 廣播,即使它們已有清單條目來請求接受這些事件的通知。在前臺執行的應用如果使用 BroadcastReceiver 請求接收通知,則仍可以在主執行緒中偵