Android版本适配

Android版本适配

Android M(6.0) 适配

运行时权限动态申请html

Android N(7.0) 适配

在Android7.0系统上,Android 框架强制执行了 StrictMode API 政策禁止向你的应用外公开 file:// URI。 若是一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常,如调用系统相机拍照录制视频,或裁切照片java

若要在应用间共享文件,能够发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此受权的最简单方式是使用 FileProvider类。android

使用FileProvider的大体步骤以下:git

1.在manifest清单文件中注册providergithub

<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.jph.takephoto.fileprovider" android:grantUriPermissions="true" android:exported="false"> 

<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> 
复制代码

exported:要求必须为false,为true则会报安全异常。grantUriPermissions:true,表示授予 URI 临时访问权限。安全

2.指定共享的目录markdown

为了指定共享的目录咱们须要在资源(res)目录下建立一个xml目录,而后建立一个名为“file_paths”(名字能够随便起,只要和在manifest注册的provider所引用的resource保持一致便可)的资源文件,内容以下:网络

<!-- 内部存储空间应用私有目录下的files/目录,等同于Context.getFilesDir() 所获取的目录路径 /data/data/包名/files目录-->
    <files-path name="DocDir" path="/" />
    <!-- 内部存储空间应用私有目录下的cache/目录,等同于Context.getCacheDir() 所获取的目录路径 /data/data/包名/cache目录-->
    <cache-path name="CacheDocDir" path="/" />
    <!--外部存储空间应用私有目录下的files/目录,等同于Context.getExternalFilesDir(null) 所获取的目录路径 /storage/sdcard/Android/data/包名/files-->
    <external-files-path name="ExtDocDir" path="/" />
    <!--外部存储空间应用私有目录下的cache/目录,等同于Context.getExternalCacheDir() /storage/sdcard/Android/data/包名/cache-->
    <external-cache-path name="ExtCacheDir" path="/" />
复制代码

3.使用FileProviderapp

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg"); 
if (!file.getParentFile().exists()){
   file.getParentFile().mkdirs(); 
}
//经过FileProvider建立一个content类型的Uri 
Uri imageUri = FileProvider.getUriForFile(context, "com.jph.takephoto.fileprovider", file);
Intent intent = new Intent(); 
//添加这一句表示对目标应用临时受权该Uri所表明的文件 
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);   
//设置Action为拍照
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
//将拍取的照片保存到指定URI
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent,1006); 
复制代码

Android O(8.0) 适配

1、通知适配框架

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);
    }
}
复制代码

删除渠道代码以下:

private void deleteNotificationChannel(String channelId){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        NotificationManager mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.deleteNotificationChannel(channelId);
    }
}
复制代码

2、后台限制执行

详见

应用在两个方面受到限制:

**后台服务限制:**处于空闲状态时,应用可使用的后台服务存在限制。 这些限制不适用于前台服务,由于前台服务更容易引发用户注意。

广播限制:除了有限的例外状况,应用没法使用清单注册隐式广播。 它们仍然能够在运行时注册这些广播,而且可使用清单注册专门针对它们的显式广播。

在大多数状况下,应用均可以使用 JobScheduler 克服这些限制。 这种方式让应用安排为在未活跃运行时执行工做,不过仍可以使系统能够在不影响用户体验的状况下安排这些做业。

private void sendNotification() {
        if (Build.VERSION.SDK_INT>=26) {
            Intent intent = new Intent(this, StopActivity.class);
            //建立NotificationChannel并与NotificationManager、Notification关联,不然系统会
            //提示Failed to post notification on channel “null”
            String channelId = "channel1";
            Notification nf = new Notification.Builder(this, channelId)
                    .setContentTitle("通知")
                    .setContentText("一个服务正在运行")
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentIntent(PendingIntent.getActivities(this, 100, new Intent[]{intent}, PendingIntent.FLAG_CANCEL_CURRENT))
                    .build();
            startForeground(10, nf);
        }
    }
 if (Build.VERSION.SDK_INT>=26) {
    startForegroundService(new Intent(this, MyService.class));
 }else{
   startService(new Intent(this, MyService.class));
 }
复制代码

关于的用法能够参考官方例子:android-JobScheduler

github.com/googlesampl…

固然还有后台位置的限制须要去注意。

详见

3、APK文件下载成功没有正常跳到应用安装界面

Android O (Android 8.0) 中,Google 移除掉了容易被滥用的“容许未知来源”应用的开关,在安装 Play Store 以外的第三方来源的 Android 应用的时候,居然没有了“容许未知来源”的检查框,若是你仍是想要安装某个被本身所信任的开发者的 app,则须要在每一次都手动授予“安装未知应用”的许可。

首先在AndroidManifest.xml 清单文件中添加安装未知来源应用的权限

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
复制代码

而后在用户点击更新时判断是否开启了该应用的“容许安装未知来源”的权限,没有的话,就引导用户去开启该应用的“容许安装未知来源”的权限

private void downloadAPK(){
      boolean hasInstallPerssion = getPackageManager().canRequestPackageInstalls();
            if (hasInstallPerssion ) {
               //安装应用的逻辑
            } else {
               //跳转至“安装未知应用”权限界面,引导用户开启权限,能够在onActivityResult中接收权限的开启结果
                Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
            }
          }

//接收“安装未知应用”权限的开启结果
@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
            downloadAPK();
        }
    }
复制代码

这样点击更新时引导用户开启“容许安装未知来源”的权限后,APK文件下载成功后也 成功的跳转到应用安装界面。第三个问题也获得了解决。

Android P(9.0) 适配

1、Http请求失败

在9.0中默认状况下启用网络传输层安全协议 (TLS),默认状况下已停用明文支持。也就是不容许使用http请求,要求使用https。

解决方法是须要咱们添加网络安全配置。首先在res 目录下新建xml文件夹,添加network_security_config.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>
复制代码

AndroidManifest.xml中的 application添加

以上这是一种简单粗暴的配置方法,要么支持http,要么不支持http。为了安全灵活,咱们能够指定支持的http域名:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
	<!-- Android 9.0 上部分域名时使用 http -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">secure.example.com</domain>
        <domain includeSubdomains="true">cdn.example1.com</domain>
    </domain-config>
</network-security-config>
复制代码

2、前台服务

Android P(9.0)要求建立一个前台服务须要请求 FOREGROUND_SERVICE 权限,不然系统会引起 SecurityException

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
复制代码

3、其余

在 Android 9 中,调用Build.SERIAL 会始终返回 UNKNOWN 以保护用户的隐私。若是你的应用须要访问设备的硬件序列号,那么须要先请求 READ_PHONE_STATE 权限,而后调用 Build.getSerial

Android Q(10.0) 适配

1、分区存储

应用只能看到本应用专有的目录(经过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用须要访问存放在应用的专有目录以及 MediaStore 以外的文件,不然最好使用分区存储。

要点: 1.Android Q文件存储机制修改为了沙盒模式 2.APP只能访问本身目录下的文件和公共媒体文件 3.Android Q版本如下机型,仍是使用老的文件存储方式 4.Android Q及以上版本机型,全部应用均须要分区存储, 因此应用须要提早确保支持分区存储

须要注意:在适配AndroidQ的时候还要兼容Q系统版本如下的,使用SDK_VERSION区分 外部存储:被分为应用私有目录以及共享目录两个部分

应用私有目录:存储应用私有数据,外部存储应用私有目录对应

1.外部存储应用私有目录对应/Android/data/包名/,内部存储应用私有目录对应/data/data/包名/ 2.应用私有目录文件访问方式与以前Android版本一致,能够经过File path获取资源。

共享目录:

1.存储其余应用可访问文件, 包含媒体文件、文档文件以及其余文件,对应设备DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目录。

2.共享目录文件须要经过MediaStore API或者Storage Access Framework方式访问。

3.MediaStore API在共享目录指定目录下建立文件或者访问应用本身建立文件,不须要申请存储权限

4.MediaStore API访问其余应用在共享目录建立的媒体文件(图片、音频、视频), 须要申请存储权限,未申请存储权限,经过ContentResolver查询不到文件Uri,即便经过其余方式获取到文件Uri,读取或建立文件会抛出异常

5.MediaStore API不可以访问其余应用建立的非媒体文件(pdf、office、doc、txt等), 只可以经过Storage Access Framework方式访问

受影响的变动

图片位置信息 一些图片会包含位置信息,由于位置对于用户属于敏感信息, Android 10应用在分区存储模式下图片位置信息默认获取不到,应用经过如下两项设置能够获取图片位置信息: 1.1在manifest中申请ACCESS_MEDIA_LOCATION 1.2调用MediaStore setRequireOriginal(Uri uri)接口更新图片Uri

兼容模式

应用未完成外部存储适配工做,能够临时以兼容模式运行, 兼容模式下应用申请存储权限,便可拥有外部存储完整目录访问权限,经过Android10以前文件访问方式运行,如下设置应用以兼容模式运行。

tagretSDK 大于等于Android 10(API level 29), 在manifest中设置requestLegacyExternalStorage属性为true

<manifest ...>
...
<application android:requestLegacyExternalStorage="true" ... >
...
</manifest>
复制代码
​	**判断兼容模式接口**
```
//返回值
//true : 应用以兼容模式运行
//false:应用以分区存储特性运行
Environment.isExternalStorageLegacy();
```

​	**File Path路径访问受影响接口**

开启分区存储新特性, Andrioid 10不可以经过File Path路径直接访问共享目录下资源,如下接口经过File 路径操做文件资源,功能会受到影响,应用须要使用MediaStore或者SAF方式访问。

**存储特性Android版本差别概览**
复制代码

适配指导

AndroidQ中使用ContentResolver进行文件的增删改查。

1)获取(建立)私有目录下的文件夹

File apkFile = context.getExternalFilesDir("apk");
复制代码

2)建立私有目录文件

生成须要下载的路径,经过输入输出流读取写入

String apkFilePath = context.getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "demo.apk");
OutputStream os = null;
try {
    os = new FileOutputStream(newFile);
    if (os != null) {
        os.write("file is created".getBytes(StandardCharsets.UTF_8));
        os.flush();
    }
} catch (IOException e) {
} finally {
    try {
        if (os != null) {
        os.close();
    }catch (IOException e1) {
    }
}
复制代码

3)建立共享目录文件或文件夹

主要是在公共目录下建立文件或文件夹拿到本地路径uri,不一样的Uri,能够保存到不一样的公共目录中。接下来使用输入输出流就能够写入文件。

重点:AndroidQ中不支持file://类型访问文件,只能经过uri方式访问。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    ContentResolver resolver = context.getContentResolver();
    ContentValues values = new ContentValues();
    values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
    values.put(MediaStore.Downloads.DESCRIPTION, fileName);
    //设置文件类型
    values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
    //注意MediaStore.Downloads.RELATIVE_PATH须要targetVersion=29,
    //故该方法只可在Android10的手机上执行
    values.put(MediaStore.Downloads.RELATIVE_PATH, "Download" + File.separator + "apk");
    Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
    String status = Environment.getExternalStorageState();
    // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
    if (status.equals(Environment.MEDIA_MOUNTED)) {
        return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);
    } else {
        return resolver.insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
    }
}
复制代码

2、定位权限

用户能够更好地控制应用什么时候能够访问设备位置。当在Android Q上运行的应用程序请求位置访问时,会经过对话框的形式给用户进行受权提示。此对话框容许用户授予对两个不一样范围的位置访问权限:在使用中(仅限前台)或始终(前台和后台)

新增权限 ACCESS_BACKGROUND_LOCATION

若是你的应用针对 Android Q 而且须要在后台运行时访问用户的位置,则必须在应用的清单文件中声明新权限

<manifest>
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>
复制代码

3、新增 ACCESS_MEDIA_LOCATION 权限

一些照片在其数据中会包含位置信息,容许用户查看拍摄照片的位置。因为此位置信息是敏感的,所以咱们想获取位置信息须要如下几步:

  • 将新的 ACCESS_MEDIA_LOCATION 权限添加到AndroidManifest
  • 获取位置信息
photoUri = MediaStore.setRequireOriginal(photoUri);
//从流中读取位置信息
InputStream stream = getContentResolver().openInputStream(photoUri);
复制代码

Android R(11.0) 适配

1、Scoped Storage(分区存储)

不过须要注意的是,应用targetSdkVersion >= 30,强制执行分区存储机制。以前在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"的适配方式已不起做用。

还有一个变化:Android 11 容许使用除 MediaStore API 以外的 API 经过文件路径直接访问共享存储空间中的媒体文件。其中包括:

  • File API
  • 原生库,例如 fopen()

若是你以前没有适配Android 10,这一点对你来讲是个好消息。Android 10在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"来适配,Android 11上直接使用File API访问媒体文件。

不过,使用原始文件路径直接访问共享存储空间中的媒体文件会重定向到 MediaStoreAPI,此次重定向会形成性能影响(随机读写慢一倍左右)。并且直接使用原始文件路径,并不会比使用 MediaStore API 有更多优点,所以官方强烈建议直接使用 MediaStore API。

固然还有一种简单粗暴的适配方法,获取外部存储管理权限。若是你的应用是手机管家、文件管理器这类须要访问大量文件的app,能够申请MANAGE_EXTERNAL_STORAGE权限,将用户引导至系统设置页面开启。代码以下:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
复制代码

2、单次权限受权

从 Android 11 开始,每当应用请求与位置信息、麦克风或摄像头相关的权限时,面向用户的权限对话框会包含仅限这一次选项。若是用户在对话框中选择此选项,系统会向应用授予临时的单次受权。

028bea05cd87e3edd4df1c6927776ae7.png

单次权限受权的应用能够在一段时间内访问相关数据,具体时间取决于应用的行为和用户的操做:

  • 当应用的 Activity 可见时,应用能够访问相关数据
  • 若是用户将应用转为后台运行,应用能够在短期内继续访问相关数据
  • 若是您在 Activity 可见时启动了一项前台服务,而且用户随后将您的应用转到后台,那么您的应用能够继续访问相关数据,直到该前台服务中止
  • 若是用户撤消单次受权(例如在系统设置中撤消),不管您是否启动了前台服务,应用都没法访问相关数据。与任何权限同样,若是用户撤消了应用的单次受权,应用进程就会终止

当用户下次打开应用而且应用中的某项功能请求访问位置信息、麦克风或摄像头时,系统会再次提示用户授予权限。

3、请求位置权限

这部分在Android 10的适配有过调整,当时规则以下:

请求ACCESS_FINE_LOCATION或 ACCESS_COARSE_LOCATION权限表示在前台时拥有访问设备位置信息的权限。在请求弹框中,选择“始终容许”表示先后台均可以获取位置信息,选择“仅在应用使用过程当中容许”只表示拥有前台的权限。

在Android 11中,请求弹框中取消了“始终容许”这一选项。也就是说默认不会授予你后台访问设备位置信息的权限。若是尝试请求ACCESS_BACKGROUND_LOCATION权限的同时请求任何其余权限,系统会抛出异常,不会向应用授予其中的任一权限。

官方给出的适配建议及缘由以下:

建议应用对位置权限执行递增请求,先请求前台位置信息访问权限,再请求后台位置信息访问权限。执行递增请求能够为用户提供更大的控制权和透明度,由于他们能够更好地了解应用中的哪些功能须要后台位置信息访问权限。

总结一下得出两点:

  • 先请求前台位置信息访问权限,再请求后台位置信息访问权限。

  • 单独请求后台位置信息访问权限,不要与其余权限一同请求。

4、软件包可见性

软件包可见性是Android 11上提高系统隐私安全性的一个新特性。它的做用是限制app随意获取其余app的信息和安装状态。避免病毒软件、间谍软件利用,引起网络钓鱼、用户安装信息泄露等安全事件。

软件包可见性是Android 11上提高系统隐私安全性的一个新特性。它的做用是限制app随意获取其余app的信息和安装状态。避免病毒软件、间谍软件利用,引起网络钓鱼、用户安装信息泄露等安全事件。

解决方法很简单,在AndroidManifest.xml 中添加queries元素,里面添加须要可见的应用包名。

<manifest package="com.example.app">
   <queries>
   <!-- 微博 -->
   <package android:name="com.sina.weibo" />
   <!-- QQ -->
   <package android:name="com.tencent.mobileqq" />
   <!-- 支付宝 -->
   <package android:name="com.eg.android.AlipayGphone" /> 
   <!-- AlipayHK -->
   <package android:name="hk.alipay.wallet" />
   </queries>
</manifest>
复制代码

除了直接添加包名的方式外,咱们能够按intent和provider来添加:

<manifest package="com.example.app">
    <queries>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="image/jpeg" />
        </intent>

        <provider android:authorities="com.example.settings.files" />
    </queries>
复制代码

具体的规则参见:管理软件包可见性

固然,还有一种简单粗暴的方式,能够直接申请权限QUERY_ALL_PACKAGES。若是你的应用须要上架Google Play,那么可能要注意相关政策。为了尊重用户隐私,建议咱们的应用按正常工做所需的最小软件包可见性来适配。

最后须要注意的是,使用queries元素须要Android Gradle 插件版本是 4.1及以上,由于旧版本的插件并不兼容此元素,出现合并 manifest 的错误。

5、前台服务类型

Android 10中,在前台服务访问位置信息,须要在对应的service中添加 location 服务类型。

一样的,Android 11中,在前台服务访问摄像头或麦克风,须要在对应的service中添加camera或microphone 服务类型。

<manifest>
   <service android:name="MyService" android:foregroundServiceType="location|microphone|camera" />
</manifest>
复制代码

这一限制的变动,使得程序没法在后台启动服务访问摄像头和麦克风。如需使用,只能是前台开启前台服务。除非有以下状况:

  • 服务由系统组件启动
  • 服务是经过应用小部件启动
  • 服务是经过与通知交互启动的
  • 服务是PendingIntent启动的,它是从另外一个可见的应用程序发送过来的
  • 服务由一个提供VoiceInteractionService的应用启动
  • 服务由一个具备START_ACTIVITIES_FROM_BACKGROUND权限的应用启动

6、权限自动重置

若是应用以 Android 11 或更高版本为目标平台而且数月未使用,系统会经过自动重置用户已授予应用的运行时敏感权限来保护用户数据。以下图所示:

b83a843f8f55dc495cc42365244762ed.png

注意上图中有一个启动自动重置的开关。若是咱们的应用有特殊须要,能够引导用户关闭它。示例代码以下:

public void checkAutoRevokePermission(Context context) {
    // 判断是否开启
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
            !context.getPackageManager().isAutoRevokeWhitelisted()) {
        // 跳转设置页 
        Intent intent = new Intent(Intent.ACTION_AUTO_REVOKE_PERMISSIONS);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setData(Uri.fromParts("package", context.getPackageName(), null));
        context.startActivity(intent);
    }
}
复制代码

7、读取手机号 若是你是经过TelecomManager的getLine1Number方法,或TelephonyManager的getMsisdn方法获取电话号码。那么在Android 11中须要增长READ_PHONE_NUMBERS权限。使用其余方法不受限。

<manifest>
    <!-- 若是应用仅在 Android 10及更低版本中使用该权限,能够添加 maxSdkVersion="29" -->
    <uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="29" />
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
</manifest>

复制代码

参考文章 Android R(11.0) 适配

相关文章
相关标签/搜索