安卓4.0

UI更新线程检查机制(区分UI线程,其他线程),非创建UI的线程不能更新UI

安卓5.0

Dalvike虚拟机改成Android核心库集和ART(Android Runtime)模式,带来了预编译,从动态编译到预编译,程序打开速度提升了几倍,缺点是安装后需要等待预编译,安装程序时间及占用内存翻倍。

安卓6.0

1. 动态权限

运行下列危险权限需要动态申请

权限组 权限名称
CALENDAR android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR
CAMERA android.permission.CAMERA
CONTACTS android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS
LOCATION android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION
MICROPHONE android.permission.RECORD_AUDIO
PHONE android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.ADD_VOICEMAIL
android.permission.WRITE_CALL_LOG
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS
android.permission.ANSWER_PHONE_CALLS(8.0新增)
android.permission.READ_PHONE_NUMBERS(8.0新增)
SENSORS android.permission.BODY_SENSORS
SMS android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
STORAGE android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE

2.Wifi相关操作

Android6.0之后,Wifi的使用更加严格。需要动态获取LOCATION权限,如果还想获取Wifi列表的话还需要打开GPS(位置信息)。

安卓7.0

1.FileProvider

在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。
要应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider类。

  1. 创建新的FileProvider

    /**
     * 继承FileProvider,防止冲突
     */
    public class RoProvider extends FileProvider {
    
    }
    
  2. 创建file_path.xml

    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <root-path name="root" path="." />
        <files-path name="files" path="" />
        <cache-path name="cache" path="" />
        <external-path name="external" path="" />
        <external-files-path name="external-files" path="" />
        <external-cache-path name="external-cache" path="" />
    </paths>
    

    各个标签代表的意义

    name path
    path名称标志字符串,不可以同名 文件夹“相对路径”,完整路径取决于当前的标签类型
    标签 路径
    -- --
    root-path 代表设备的根目录new File("/")
    files-path 代表context.getFilesDir()
    cache-path 代表context.getCacheDir()
    external-path 代表Environment.getExternalStorageDirectory()
    external-files-path 代表context.getExternalFilesDirs()
    external-cache-path 代表context.getExternalCacheDirs()

    禁用FileProvider授权

    /**
     * 需要在Application中执行
     */
    private void detectFileUriExposure() {
        Builder builder = new Builder();
        StrictMode.setVmPolicy(builder.build());
        builder.detectFileUriExposure();
    }
    

2.APK signature scheme v2

Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。在默认情况下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 会使用 APK Signature Scheme v2 和传统签名方案来签署您的应用。

  • 只勾选V1签名就是传统方案签署,但是在 Android 7.0 上不会使用V2安全的验证方式。
  • 只勾选V2签名7.0以下会显示未安装,Android 7.0 上则会使用了V2安全的验证方式。
  • 同时勾选V1和V2则所有版本都没问题。

3.org.apache不支持问题

安卓源码已经去除了,这部分网络请求的代码,解决办法是build.gradle里面加上这句话

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

或者在AndroidManifest.xml添加下面的配置

<uses-library
    android:name="org.apache.http.legacy"
    android:required="false" />

4.SharedPreferences调整

// MODE_WORLD_READABLE:Android 7.0以后不能使用这个获取,会闪退
// 应修改成MODE_PRIVATE
SharedPreferences read = getSharedPreferences(RELEASE_POOL_DATA, MODE_WORLD_READABLE);

5.三个广播被禁止监听或发送

在后台时不再能接收到 CONNECTIVITY_CHANGE 广播,前台不影响。
不能发送或是接收新增图片(ACTION_NEW_PICTURE)和新增视频(ACTION_NEW_VIDEO) 的广播。

6.动态运行SO库

会报错,需要targetApi23及以下才可运行。8.0后会彻底失去支持,实测华为8.0后不能支持,小米等大部分国产手机还是支持的,可能厂家自己App有这个需求。

安卓8.0

1.Notification通知权限

Android 8.0之后通知权限默认都是关闭的,无法默认开启以及通过程序去主动开启,需要程序员读取权限开启情况,然后提示用户去开启。

  • 判断权限是否开启
    /**
     * 判断通知权限是否开启
     * @param context 上下文
     */
    public static boolean isNotificationEnabled(Context context){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            ApplicationInfo appInfo = context.getApplicationInfo();
            String pkg = context.getApplicationContext().getPackageName();
            int uid = appInfo.uid;
    
            try {
                Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
                Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
                Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");
                int value = (Integer) opPostNotificationValue.get(Integer.class);
                return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
            } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
                return true;
            }
        } else {
            return true;
        }
    }
    
  • 前往设置开启权限
    /**
     * 打开设置页面打开权限
     *
     * @param activity activity
     * @param requestCode 这里的requestCode和onActivityResult中requestCode要一致
     */
    public static void startSettingActivity(@NonNull Activity activity, int requestCode) {
        try {
            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + activity.getPackageName()));
            intent.addCategory(Intent.CATEGORY_DEFAULT);
            activity.startActivityForResult(intent, requestCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    

2.Notification(通知适配)

Android 8.0中,为了更好的管制通知的提醒,不想一些不重要的通知打扰用户,新增了通知渠道,用户可以根据渠道来屏蔽一些不想要的通知。

  • 创建通知
    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationManager notificationManager = (NotificationManager)
                    getSystemService(Context.NOTIFICATION_SERVICE);
            //分组(可选)
            //groupId要唯一
            String groupId = "group_001";
            NotificationChannelGroup group = new NotificationChannelGroup(groupId, "广告");
            //创建group
            notificationManager.createNotificationChannelGroup(group);
            //channelId要唯一
            String channelId = "channel_001";
            NotificationChannel adChannel = new NotificationChannel(channelId,
                    "推广信息", NotificationManager.IMPORTANCE_DEFAULT);
            //补充channel的含义(可选)
            adChannel.setDescription("推广信息");
            //将渠道添加进组(先创建组才能添加)
            adChannel.setGroup(groupId);
            //创建channel
            notificationManager.createNotificationChannel(adChannel);
            //创建通知时,标记你的渠道id
            Notification notification = new Notification.Builder(MainActivity.this, channelId)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                    .setContentTitle("一条新通知")
                    .setContentText("这是一条测试消息")
                    .setAutoCancel(true)
                    .build();
            notificationManager.notify(1, notification);
        }
    }
    

3.自适应启动图标

从Android 8.0系统开始,应用程序的图标被分为了两层:前景层和背景层。

前景用来展示应用图标的Logo,背景用来衬托应用图标的Logo。需要注意的是,背景层在设计的时候只允许定义颜色和纹理,但是不能定义形状。

注意图标图层的大小,两层的尺寸必须为108x108dp,前景图层中间的72x72dp图层就是在手机界面上展示的应用图标范围。这样系统在四面各留出18dp以产生有趣的视觉效果。

  • mipmap-anydpi-v26文件夹
    <?xml version="1.0" encoding="utf-8"?>
    <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
        <background android:drawable="@drawable/ic_launcher_background" />
        <foreground android:drawable="@drawable/ic_launcher_foreground" />
    </adaptive-icon>
    
    

4.安装APK

Android 8.0去除了“允许未知来源”选项,如果我们的App具备安装App的功能,那么AndroidManifest文件需要包含REQUEST_INSTALL_PACKAGES权限,未声明此权限的应用将无法安装其他应用。当然,如果你不想添加这个权限,也可以通过getPackageManager().canRequestPackageInstalls()查询是否有此权限,没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

5.静态广播无法正常接收

  • 发送静态广播的特殊处理
    Intent intent = new Intent( "广播的action" );
    intent.setComponent( new ComponentName( "包名(如:com.yhd.rocket)","接收器的完整路径(如:com.yhd.rocket.receiver.RoReceiver)" ) );
    sendBroadcast(intent);
    

安卓9.0

1.刘海屏API支持

Android 9 支持最新的全面屏,其中包含为摄像头和扬声器预留空间的屏幕缺口。 通过 DisplayCutout类可确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些屏幕缺口区域是否存在及其位置,使用 getDisplayCutout() 函数。

  • 取区域位置及位置
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        View decorView = getWindow().getDecorView();
        WindowInsets rootWindowInsets = decorView.getRootWindowInsets();
        if (rootWindowInsets != null) {
            DisplayCutout cutout = rootWindowInsets.getDisplayCutout();
            List<Rect> boundingRects = cutout.getBoundingRects();
            if (boundingRects != null && boundingRects.size() > 0) {
                String msg = "";
                for (Rect rect : boundingRects) {
                    msg = msg +"left-" + rect.left;
                    Log.d(TAG, msg);
                }
             }
        }
    }
    
    
  • 新窗口布局模式,允许应用程序请求是否在挖孔区域布局
    class WindowManager.LayoutParams {
        //布局参数
        int layoutInDisplayCutoutMode;
        //默认情况下,全屏窗口不会使用到挖孔区域,非全屏窗口可正常使用挖孔区域。
        final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
        //窗口声明使用挖孔区域
        final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        //窗口声明不使用挖孔区域
        final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
    }
    
  • 设置代码
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
    getWindow().setAttributes(lp);
    

2.使用不安全网络

使用非https加密网络会报异常,需要manifest的application进下列配置

android:networkSecurityConfig="@xml/nsc"
<network-security-config>
     <!--默认允许所有网址使用非安全连接-->
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

3.View绘制api

在自定义绘制View过程中会遇到 Android 9.0 兼容问题导致的Crash,解决方案:

if (Build.VERSION.SDK_INT >= 26){
  canvas.clipPath(mPath); 
} else {
  canvas.clipPath(mPath, Region.Op.REPLACE);
}

4.前台服务需要添加权限

在安卓9.0版本之后,必须要授予FOREGROUND_SERVICE权限,才能够使用前台服务,否则会抛出异常。对此,我们只需要在AndroidManifest添加对应的权限即可,这个权限是普通权限,不需要动态申请。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

5.全面限制静态广播的接收

升级安卓9.0之后,隐式广播将会被全面禁止,在AndroidManifest中注册的Receiver将不能够生效,你需要在应用中进行动态注册。

MyReceiver myReceiver = new MyReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(MY_ACTION);
registerReceiver(myReceiver, intentFilter);

6.非 SDK 接口访问限制(反射修改Hide变量或者私有方法)

此方法在安卓9.0版本将不能够正常运行,会抛出NoSuchFieldException,对于诸如此类的调用官方private方法或者@hide方法,都将不能使用。

try {
    //通过反射的方式来更改dialog中文字大小、颜色
    Field mAlert = AlertDialog.class.getDeclaredField("mAlert");
    mAlert.setAccessible(true);
    Object mAlertController = mAlert.get(normalDialog);
    Field mMessage = mAlertController.getClass().getDeclaredField("mMessageView");
    mMessage.setAccessible(true);
    TextView mMessageView = (TextView) mMessage.get(mAlertController);
    mMessageView.setTextSize(23);
    mMessageView.setTextColor(Color.RED);
    Field mTitle = mAlertController.getClass().getDeclaredField("mTitleView");
    mTitle.setAccessible(true);
    TextView mTitleView = (TextView) mTitle.get(mAlertController);
    mTitleView.setTextSize(20);
    mTitleView.setTextColor(Color.RED);
} catch (Exception e){
    Toast.makeText(NotSDKInterfaceActivity.this,e.getLocalizedMessage(),Toast.LENGTH_LONG).show();
}

安卓10.0

Scoped Storage(分区存储)

  • 特定目录(App-specific),使用getExternalFilesDir(String type)或 getExternalCacheDir()方法访问。无需权限,且卸载应用时会自动删除。
  • 照片、视频、音频这类媒体文件。使用MediaStore 访问,访问其他应用的媒体文件时需要READ_EXTERNAL_STORAGE权限。
  • 其他目录,使用存储访问框架SAF(Storage Access Framwork)

权限变化

  • 在后台运行时访问设备位置信息需要权限

    Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限(危险权限)。该权限允许应用程序在后台访问位置。如果请求此权限,则还必须请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限。只请求此权限无效果。官方推荐使用前台服务来实现,在前台服务中获取位置信息。

    <service
        android:name="MyNavigationService"
        android:foregroundServiceType="location">
    </service>
    
  • 电话、蓝牙和WLAN的API需要精确位置权限

    上述部分类和方法中必须具有 ACCESS_FINE_LOCATION 权限才能使用。

  • 新增ACCESS_MEDIA_LOCATION权限

    如果你要获取图片中的地理位置信息,需要申请ACCESS_MEDIA_LOCATION权限,并使用MediaStore.setRequireOriginal()获取。

  • 废弃PROCESS_OUTGOING_CALLS权限

    呼出电话的监听

后台启动 Activity 的限制

简单解释就是应用处于后台时,无法启动Activity。因为此项行为变更适用于在 Android 10 上运行的所有应用,所以这一限制导致最明显的问题就是点击推送信息时,有些应用无法进行正常的跳转(具体的实现问题导致)。所以针对这类问题,全屏 intent,注意设置最高优先级和添加USE_FULL_SCREEN_INTENT权限,这是一个普通权限。比如微信来语音或者视频通话时,弹出的接听页面就是使用这一功能。

深色主题

Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并不是强制适配项,但是它可以带给用户更好的体验:

  • 可大幅减少耗电量。 OLED 屏幕中每个像素都是自主发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭来达到省电的效果。
  • 为弱视以及对强光敏感的用户提高可视性。深色可以降低屏幕的整体视觉亮度,减少对眼睛的视觉压力。
  • 让所有人都可以在光线较暗的环境中更轻松地使用设备
    // 获取uimode系统服务
    UiModeManager uiModeManager = (UiModeManager)
    getSystemService(Context.UI_MODE_SERVICE);                                             
    // 获取设置状态
    int currentMode = uiModeManager.getNightMode();                                               
    // 设置夜间状态
    uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_AUTO); // ⾃动
    uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_YES); // 启⽤
    uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO); // 停⽤
    

标识符和数据

  • 对不可重置的设备标识符实施了限制

    Build.getSerial()、TelephonyManager.getImei()/getXXId()/getXXNumber()等唯一标识符方法应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能正常使用。

  • 限制了对剪贴板数据的访问权限

    除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。

  • 对启用和停用 WLAN 实施了限制

    以 Android 10 或更高版本为目标平台的应用无法启用或停用 WLAN。WifiManager.setWifiEnabled()方法始终返回 false。如果您需要提示用户启用或停用 WLAN,请使用设置面板。

Android11.0

Scoped Storage(分区存储)

当您将应用更新为以 Android 11 为目标平台后,将无法使用 requestLegacyExternalStorage 来停用分区存储。

使用原始路径访问文件

从 Android 11 开始,具有 READ_EXTERNAL_STORAGE 权限的应用可以使用直接文件路径和原生库来读取设备的媒体文件。通过这项新功能,应用可以更顺畅地使用第三方媒体库。

当您访问现有媒体文件时,您可以使用您的逻辑中** DATA** 列的值。这是因为,此值包含有效的文件路径。但是,不要假设文件始终可用。请准备好处理可能发生的任何基于文件的 I/O 错误。另一方面,如需创建或更新媒体文件,请勿使用 DATA 列的值。请改用** DISPLAY_NAME** 和 RELATIVE_PATH 列的值。

详见:https://developer.android.google.cn/preview/privacy/storage#change-details。

系统分区使用A/B双分区

A/B 设置是指包含系统映像、启动映像等的重复分区。具有重复的 A/B 分区的设备允许无缝更新过程,因为包含相同系统映像的非活动分区会在后台更新,而设备仍然可用,这要归功于包含工作区映像的活动分区。但是,在更新过程中,没有 A/B 分区的设备会将用户锁定在手机之外。

如果更新成功,则两个分区在重新引导后会交换位置,从而将以前的非活动分区转换为使用最新软件的活动分区。但是,在更新失败的情况下,设备仍可以使用旧的活动分区启动。

所有文件访问权限(手机sd卡下任意路径)

某些应用的核心用例需要访问大量的文件,如文件管理操作或备份和恢复操作。这些应用可通过执行以下操作来获取“所有文件访问权限”:

  1. 声明 MANAGE_EXTERNAL_STORAGE 权限。
  2. 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为应用启用以下选项:授予所有文件的管理权限。

Android12.0

动态颜色

在安卓12的系统中,Material Design主题将支持动态主题颜色,将根据用户桌面壁纸的主题色,自动应用白天、黑夜两套主题色。

新功能和API

  1. 用于接收内容的统一API

    当前,用户更喜欢图片、视频等富有表现力的内容,但在应用中插入和移动并非易事。为了使应用能够轻松地接收富媒体内容,

    Android 12 引入了全新的统一 API,您可以从任何可用来源(剪贴板粘贴、键盘或拖放操作)接收富媒体内容。

  2. 实现代码

    如需使用该 API,请先指定您的应用可以处理哪些类型的内容,以开始实现该监听器:

    public class MyReceiver implements OnReceiveContentListener {
         public static final String[] MIME_TYPES = new String[] 
         {"image/*", "video/*"};
         // ...
    
  3. 实现效果

Android13.0

通知的运行时权限

在之前版本中我们应用如果需要弹通知的话只需要通过 NotificationManager 即可直接进行弹出,不需要任何权限,当然后面用户可以手动关闭通知,在 Android 13(T-33)中终于引入了新的运行时权限——通知权限:POST_NOTIFICATIONS。

如果用户拒绝通知权限,他们仍会在前台服务 (FGS) 任务管理器中看到与这些前台服务相关的通知,但不会在抽屉式通知栏中看到这些通知。

这个更改对许多应用都有关系,只要你的应用会弹通知,那么如果要适配 Android 13 的话就都需要进行适配,当然适配方法很简单,再按照别的运行时权限适配下新的通知权限即可。

检查您的应用能否发送通知

如果想要确认用户是否已启用通知,可以调用

NotificationManager.areNotificationsEnabled() 来进行判断。

附近 Wi-Fi 设备的新运行时权限

在以前的 Android 版本中,需要 ACCESS_FINE_LOCATION 权限,应用才能完成与热点相关的多个常见 Wi-Fi 用例、Wi-Fi 直连、Wi-Fi RTT 等。

由于用户很难将位置信息权限与 Wi-Fi 功能相关联,因此 Android 13(T-33)在 NEARBY_DEVICES 权限组中引入了新的运行时权限,适用于管理设备与附近 Wi-Fi 接入点连接情况的应用。此权限 (NEARBY_WIFI_DEVICES) 可满足这些 Wi-Fi 用例。

只要应用不通过 Wi-Fi API 推导物理位置,那么在 Android 13 或更高版本为目标平台并使用 Wi-Fi API 的时候就可以请求 NEARBY_WIFI_DEVICES 而不是 ACCESS_FINE_LOCATION。

细化的媒体权限

如果要将应用升级为 Android 13 ,必须请求一个或多个新权限,Android 13 中将媒体权限细分为了图片、视频和音频文件,而不是之前的 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限。请求的权限集取决于应用需要访问的媒体类型,如下图所示:

媒体类型 请求权限
图片和照片 READ_MEDIA_IMAGES
视频 READ_MEDIA_VIDEO
音频文件 READ_MEDIA_AUDIO

注意:如果应用只需要访问图片、照片和视频,应该考虑使用照片选择器(下面会介绍),而不是声明 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限,还有,申请了最新的三个权限的话应用就无需再声明 WRITE_EXTERNAL_STORAGE 权限了。

下面来看下在 AndroidManifest.xml 中应该如何进行修改:

<manifest ...>
    <!-- Android 13 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

    <!-- Required to maintain app compatibility. -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                     android:maxSdkVersion="32" />
    <application ...>
        ...
    </application>
</manifest>

精确闹钟的新权限

如果升级到 Android 13 ,可以使用自动授予应用的 USE_EXACT_ALARM 权限。不过,一般是系统应用才可以使用,因为即将推出的 Google Play 政策会阻止应用使用 USE_EXACT_ALARM 权限,除非应用为日历或者时钟这样的系统应用(国内另说)。

如果应用设置了精确闹钟,但又不是系统日历或时钟的话,还是继续声明 SCHEDULE_EXACT_ALARM权限,并要为用户拒绝授予应用相应访问权限的情况做好准备。

开发者可降级权限

从 Android 13 开始,应用可以撤消先前由系统或用户授予的运行时权限。开发者可以:

  • 撤消未使用的权限。
  • 遵循权限最佳做法,从而提高用户信任度。可以向用户显示一个对话框,其中会显示应用主动撤消的权限。

如需撤消特定运行时权限,请将该权限的名称传入 revokeSelfPermissionOnKill()。如需同时撤消一组运行时权限,请将这组权限的名称传入 revokeSelfPermissionsOnKill() 。撤消是异步发生的,会终止与应用的 UID 相关联的所有进程。

为了使系统撤消权限,必须终止与应用关联的所有进程。当调用该 API 时,系统会确定何时可以安全终止这些进程。通常,系统会等待应用有较长时间在后台运行,而不是在前台运行时。

后台使用身体传感器新的权限

Android 13 中引入了“在使用时”访问身体传感器(例如心率、体温和血氧饱和度)的概念,如果要升级为 Android 13,并且在后台运行时需要访问身体传感器信息,那么除了现有的 BODY_SENSORS 权限外,还必须声明新的 BODY_SENSORS_BACKGROUND 权限。

如何申请运行时权限

官方对申请权限这块的代码进行了重写,使用起来并不比那些三方库复杂,甚至更加简单,下面来看下使用方法吧:

申请单个权限

val requestPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // 同意
        } else {
            // 拒绝
        }
    }

when {
    ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.CAMERA
    ) == PackageManager.PERMISSION_GRANTED -> {
        // 当前拥有这个权限
    }
    shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
        // 告诉用户为啥要申请这个权限
    }
    else -> {
        // 申请权限
        requestPermissionLauncher.launch(
            Manifest.permission.CAMERA
        )
    }
}

代码比较容易理解,官方新封装的权限申请代码还是挺好的,无需咱们再自己处理 onRequestPermissionsResult 中的回调信息。

申请多个权限

val requestPermissionsLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
        it.forEach { (name, success) ->
            if (success) {
                // 同意
            } else {
                // 拒绝
            }
        }
    }
when {
    // ... 
    // 这块和上面基本一致
    else -> {
        // 申请多个权限,数组展示
        requestPermissionsLauncher.launch(
            arrayOf(Manifest.permission.CAMERA)
        )
    }
}

剪贴板中隐藏敏感内容

从 Android 13 开始,将内容添加到剪贴板时,系统会显示标准视觉确认界面。新确认界面会执行以下操作:

  • 确认内容已成功复制。
  • 提供所复制内容的预览。

在 Android 12L(32)及更低版本中,用户经常不确定他们是否成功复制了内容或者复制了什么内容。

此功能可将应用在用户复制内容后显示的各种通知标准化,并让用户可以更好地控制剪贴板。

如果应用允许用户将敏感内容(例如密码或信用卡信息)复制到剪贴板,则必须在调用 ClipboardManager.setPrimaryClip() 之前向 ClipData 的 ClipDescription 添加一个标志。添加此标志可阻止敏感内容出现在内容预览中。

val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager

// When your app targets API level 33 or higher
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app targets a lower API level
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}

照片选择器

Android 13(T-33)支持新的照片选择器工具。此工具为用户提供了一种安全的内置媒体文件选择方式,让其无需向应用授予对整个媒体库的访问权限。

照片选择器提供了一个可浏览、可搜索的界面,其中按日期(从最近到最早)顺序向用户呈现其媒体库中的文件。可以指定用户只能看到照片或只能看到视频,并且默认情况下,允许的媒体选择量上限设置为 1。

定义分享限制

应用可以声明 android.provider.extra.PICK_IMAGES_MAX 的值,该值表示在向用户显示时照片选择器中显示的媒体文件数量上限。

需要注意的是:如果选择的上限为 1 张,照片选择器会以半屏模式打开。

选择单张照片或单个视频

先来看看如何选择单张照片吧:

val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
// 用户可以选择一张照片或一个视频。
startActivityForResult(intent, PHOTO_PICKER_REQUEST_CODE)

选择多张照片或多个视频

如果应用的用例需要用户选择多张照片或多个视频,可以使用 EXTRA_PICK_IMAGES_MAX extra 指定照片选择器中应显示照片的数量上限,如以下代码段中所示:

// 最大选择数量
val maxNumPhotosAndVideos = 10
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxNumPhotosAndVideos)
startActivityForResult(intent, PHOTO_PICKER_MULTI_SELECT_REQUEST_CODE)

请注意,可指定为文件数量上限的最大数字存在平台限制。如需访问此限制,请调用 MediaStore.getPickImagesMaxLimit()。

处理照片选择器结果

照片选择器启动后,使用新的 ACTION_PICK_IMAGES intent 来处理结果。该选择器会返回一组 URI:

// 处理来自照片选择器的回调。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode != Activity.RESULT_OK) return
    when (requestCode) {
            REQUEST_PHOTO_PICKER_SINGLE_SELECT -> {
            // 获取单个选择的照片选择器响应
            val currentUri: Uri = data.data
            // 处理照片或视频的URI.
            return
        }
                REQUEST_PHOTO_PICKER_MULTI_SELECT -> {
            // Get photo picker response for multi select.
            var i = 0
            while (i < data.clipData!!.itemCount) {
                val uri = data.clipData.getItemAt[i]
                // 处理照片或视频的URI.
        }
            return

默认情况下,照片选择器会既显示照片又显示视频。咱们可以在 setType() 方法中设置 MIME 类型,以便按“仅显示照片”或“仅显示视频”进行过滤。来看看代码如何实现吧:

val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
// 只显示视频
intent.type = "video/*"
startActivityForResult(intent, PHOTO_PICKER_VIDEO_SINGLE_SELECT_REQUEST_CODE)

// 只显示图片
// images only - intent.type = "images/*"

应用内语言选择器

Android 13 在手机设置中新增了一个集中设置选项,用于设置各应用语言偏好设定。如果你的应用支持多种语言,官方强烈建议我们在应用的清单中声明 android:localeConfig 属性,这样用户就可以在同一位置像更改其他应用的语言设置一样更改应用的语言设置。

此外,当前使用自定义应用内语言选择器的应用应改用适用于各应用语言偏好设定功能的新 API。使用这些新 API 有助于确保用户无论是继续通过应用内语言选择器选择语言,还是通过手机设置选择语言,都能以其首选语言查看应用。当然,如果不支持多种语言的应用将不受这些变更的影响。

如何使用

  1. 创建一个名为 res/xml/locales_config.xml 的文件,并指定您的应用的语言,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
       <locale android:name="zh"/>
       <locale android:name="en"/>
    </locale-config>
    
  2. 在清单中,添加一行指向这个新文件的代码:
    <manifest
        ...
        <application
            ...
            android:localeConfig="@xml/locales_config">
        </application>
    </manifest>
    

如何在设置中进行设置

用户可以通过新的系统设置为每个应用选择首选语言。他们可以通过以下两种方式访问这些设置:

通过系统设置访问

设置 > 系统 > 语言和输入法 > 应用语言 >(选择一款应用)

通过应用设置访问

设置 > 应用 >(选择一款应用)> 语言

处理应用内语言选择器

如需设置用户的首选语言,需要让用户在语言选择器中选择语言区域,然后在系统中设置该值:

val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags("xx-YY")
// 注意:需要在主线程上调用它,因为它可能需要Activity.restart()
AppCompatDelegate.setApplicationLocales(appLocale)

如需支持搭载 Android 12(S-32)及更低版本的设备,请在应用的 AppLocalesMetadataHolderService 服务的清单条目中将 autoStoreLocales 值设置为 true 并将 android:enabled 设置为 false,以指示 AndroidX 处理语言区域存储空间,如以下代码段所示:

<application
  ...
  <service
    android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
    android:enabled="false"
    android:exported="false">
    <meta-data
      android:name="autoStoreLocales"
      android:value="true" />
  </service>
  ...
</application>

Android13.0

通知的运行时权限

在之前版本中我们应用如果需要弹通知的话只需要通过 NotificationManager 即可直接进行弹出,不需要任何权限,当然后面用户可以手动关闭通知,在 Android 13(T-33)中终于引入了新的运行时权限——通知权限:POST_NOTIFICATIONS。

如果用户拒绝通知权限,他们仍会在前台服务 (FGS) 任务管理器中看到与这些前台服务相关的通知,但不会在抽屉式通知栏中看到这些通知。

这个更改对许多应用都有关系,只要你的应用会弹通知,那么如果要适配 Android 13 的话就都需要进行适配,当然适配方法很简单,再按照别的运行时权限适配下新的通知权限即可。

检查您的应用能否发送通知

如果想要确认用户是否已启用通知,可以调用

NotificationManager.areNotificationsEnabled() 来进行判断。

附近 Wi-Fi 设备的新运行时权限

在以前的 Android 版本中,需要 ACCESS_FINE_LOCATION 权限,应用才能完成与热点相关的多个常见 Wi-Fi 用例、Wi-Fi 直连、Wi-Fi RTT 等。

由于用户很难将位置信息权限与 Wi-Fi 功能相关联,因此 Android 13(T-33)在 NEARBY_DEVICES 权限组中引入了新的运行时权限,适用于管理设备与附近 Wi-Fi 接入点连接情况的应用。此权限 (NEARBY_WIFI_DEVICES) 可满足这些 Wi-Fi 用例。

只要应用不通过 Wi-Fi API 推导物理位置,那么在 Android 13 或更高版本为目标平台并使用 Wi-Fi API 的时候就可以请求 NEARBY_WIFI_DEVICES 而不是 ACCESS_FINE_LOCATION。

细化的媒体权限

如果要将应用升级为 Android 13 ,必须请求一个或多个新权限,Android 13 中将媒体权限细分为了图片、视频和音频文件,而不是之前的 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限。请求的权限集取决于应用需要访问的媒体类型,如下图所示:

媒体类型 请求权限
图片和照片 READ_MEDIA_IMAGES
视频 READ_MEDIA_VIDEO
音频文件 READ_MEDIA_AUDIO

注意:如果应用只需要访问图片、照片和视频,应该考虑使用照片选择器(下面会介绍),而不是声明 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限,还有,申请了最新的三个权限的话应用就无需再声明 WRITE_EXTERNAL_STORAGE 权限了。

下面来看下在 AndroidManifest.xml 中应该如何进行修改:

<manifest ...>
    <!-- Android 13 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

    <!-- Required to maintain app compatibility. -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                     android:maxSdkVersion="32" />
    <application ...>
        ...
    </application>
</manifest>

精确闹钟的新权限

如果升级到 Android 13 ,可以使用自动授予应用的 USE_EXACT_ALARM 权限。不过,一般是系统应用才可以使用,因为即将推出的 Google Play 政策会阻止应用使用 USE_EXACT_ALARM 权限,除非应用为日历或者时钟这样的系统应用(国内另说)。

如果应用设置了精确闹钟,但又不是系统日历或时钟的话,还是继续声明 SCHEDULE_EXACT_ALARM权限,并要为用户拒绝授予应用相应访问权限的情况做好准备。

开发者可降级权限

从 Android 13 开始,应用可以撤消先前由系统或用户授予的运行时权限。开发者可以:

  • 撤消未使用的权限。
  • 遵循权限最佳做法,从而提高用户信任度。可以向用户显示一个对话框,其中会显示应用主动撤消的权限。

如需撤消特定运行时权限,请将该权限的名称传入 revokeSelfPermissionOnKill()。如需同时撤消一组运行时权限,请将这组权限的名称传入 revokeSelfPermissionsOnKill() 。撤消是异步发生的,会终止与应用的 UID 相关联的所有进程。

为了使系统撤消权限,必须终止与应用关联的所有进程。当调用该 API 时,系统会确定何时可以安全终止这些进程。通常,系统会等待应用有较长时间在后台运行,而不是在前台运行时。

后台使用身体传感器新的权限

Android 13 中引入了“在使用时”访问身体传感器(例如心率、体温和血氧饱和度)的概念,如果要升级为 Android 13,并且在后台运行时需要访问身体传感器信息,那么除了现有的 BODY_SENSORS 权限外,还必须声明新的 BODY_SENSORS_BACKGROUND 权限。

如何申请运行时权限

官方对申请权限这块的代码进行了重写,使用起来并不比那些三方库复杂,甚至更加简单,下面来看下使用方法吧:

申请单个权限

val requestPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // 同意
        } else {
            // 拒绝
        }
    }

when {
    ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.CAMERA
    ) == PackageManager.PERMISSION_GRANTED -> {
        // 当前拥有这个权限
    }
    shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
        // 告诉用户为啥要申请这个权限
    }
    else -> {
        // 申请权限
        requestPermissionLauncher.launch(
            Manifest.permission.CAMERA
        )
    }
}

代码比较容易理解,官方新封装的权限申请代码还是挺好的,无需咱们再自己处理 onRequestPermissionsResult 中的回调信息。

申请多个权限

val requestPermissionsLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
        it.forEach { (name, success) ->
            if (success) {
                // 同意
            } else {
                // 拒绝
            }
        }
    }
when {
    // ... 
    // 这块和上面基本一致
    else -> {
        // 申请多个权限,数组展示
        requestPermissionsLauncher.launch(
            arrayOf(Manifest.permission.CAMERA)
        )
    }
}

剪贴板中隐藏敏感内容

从 Android 13 开始,将内容添加到剪贴板时,系统会显示标准视觉确认界面。新确认界面会执行以下操作:

  • 确认内容已成功复制。
  • 提供所复制内容的预览。

在 Android 12L(32)及更低版本中,用户经常不确定他们是否成功复制了内容或者复制了什么内容。

此功能可将应用在用户复制内容后显示的各种通知标准化,并让用户可以更好地控制剪贴板。

如果应用允许用户将敏感内容(例如密码或信用卡信息)复制到剪贴板,则必须在调用 ClipboardManager.setPrimaryClip() 之前向 ClipData 的 ClipDescription 添加一个标志。添加此标志可阻止敏感内容出现在内容预览中。

val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager

// When your app targets API level 33 or higher
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app targets a lower API level
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}

照片选择器

Android 13(T-33)支持新的照片选择器工具。此工具为用户提供了一种安全的内置媒体文件选择方式,让其无需向应用授予对整个媒体库的访问权限。

照片选择器提供了一个可浏览、可搜索的界面,其中按日期(从最近到最早)顺序向用户呈现其媒体库中的文件。可以指定用户只能看到照片或只能看到视频,并且默认情况下,允许的媒体选择量上限设置为 1。

定义分享限制

应用可以声明 android.provider.extra.PICK_IMAGES_MAX 的值,该值表示在向用户显示时照片选择器中显示的媒体文件数量上限。

需要注意的是:如果选择的上限为 1 张,照片选择器会以半屏模式打开。

选择单张照片或单个视频

先来看看如何选择单张照片吧:

val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
// 用户可以选择一张照片或一个视频。
startActivityForResult(intent, PHOTO_PICKER_REQUEST_CODE)

选择多张照片或多个视频

如果应用的用例需要用户选择多张照片或多个视频,可以使用 EXTRA_PICK_IMAGES_MAX extra 指定照片选择器中应显示照片的数量上限,如以下代码段中所示:

// 最大选择数量
val maxNumPhotosAndVideos = 10
val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxNumPhotosAndVideos)
startActivityForResult(intent, PHOTO_PICKER_MULTI_SELECT_REQUEST_CODE)

请注意,可指定为文件数量上限的最大数字存在平台限制。如需访问此限制,请调用 MediaStore.getPickImagesMaxLimit()。

处理照片选择器结果

照片选择器启动后,使用新的 ACTION_PICK_IMAGES intent 来处理结果。该选择器会返回一组 URI:

// 处理来自照片选择器的回调。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode != Activity.RESULT_OK) return
    when (requestCode) {
            REQUEST_PHOTO_PICKER_SINGLE_SELECT -> {
            // 获取单个选择的照片选择器响应
            val currentUri: Uri = data.data
            // 处理照片或视频的URI.
            return
        }
                REQUEST_PHOTO_PICKER_MULTI_SELECT -> {
            // Get photo picker response for multi select.
            var i = 0
            while (i < data.clipData!!.itemCount) {
                val uri = data.clipData.getItemAt[i]
                // 处理照片或视频的URI.
        }
            return

默认情况下,照片选择器会既显示照片又显示视频。咱们可以在 setType() 方法中设置 MIME 类型,以便按“仅显示照片”或“仅显示视频”进行过滤。来看看代码如何实现吧:

val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
// 只显示视频
intent.type = "video/*"
startActivityForResult(intent, PHOTO_PICKER_VIDEO_SINGLE_SELECT_REQUEST_CODE)

// 只显示图片
// images only - intent.type = "images/*"

应用内语言选择器

Android 13 在手机设置中新增了一个集中设置选项,用于设置各应用语言偏好设定。如果你的应用支持多种语言,官方强烈建议我们在应用的清单中声明 android:localeConfig 属性,这样用户就可以在同一位置像更改其他应用的语言设置一样更改应用的语言设置。

此外,当前使用自定义应用内语言选择器的应用应改用适用于各应用语言偏好设定功能的新 API。使用这些新 API 有助于确保用户无论是继续通过应用内语言选择器选择语言,还是通过手机设置选择语言,都能以其首选语言查看应用。当然,如果不支持多种语言的应用将不受这些变更的影响。

如何使用

  1. 创建一个名为 res/xml/locales_config.xml 的文件,并指定您的应用的语言,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
       <locale android:name="zh"/>
       <locale android:name="en"/>
    </locale-config>
    
  2. 在清单中,添加一行指向这个新文件的代码:
    <manifest
        ...
        <application
            ...
            android:localeConfig="@xml/locales_config">
        </application>
    </manifest>
    

如何在设置中进行设置

用户可以通过新的系统设置为每个应用选择首选语言。他们可以通过以下两种方式访问这些设置:

通过系统设置访问

设置 > 系统 > 语言和输入法 > 应用语言 >(选择一款应用)

通过应用设置访问

设置 > 应用 >(选择一款应用)> 语言

处理应用内语言选择器

如需设置用户的首选语言,需要让用户在语言选择器中选择语言区域,然后在系统中设置该值:

val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags("xx-YY")
// 注意:需要在主线程上调用它,因为它可能需要Activity.restart()
AppCompatDelegate.setApplicationLocales(appLocale)

如需支持搭载 Android 12(S-32)及更低版本的设备,请在应用的 AppLocalesMetadataHolderService 服务的清单条目中将 autoStoreLocales 值设置为 true 并将 android:enabled 设置为 false,以指示 AndroidX 处理语言区域存储空间,如以下代码段所示:

<application
  ...
  <service
    android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
    android:enabled="false"
    android:exported="false">
    <meta-data
      android:name="autoStoreLocales"
      android:value="true" />
  </service>
  ...
</application>