Android6.0,7.0适配

Android6.0适配

从Android 6.0 MarshMallow开始,Android支持动态权限管理,即有些权限需要在使用到的时候动态申请。

这些权限会关系到用户的隐私或影响到其他应用的运行,这些危险权限,谷歌还做了一个权限组,以分组的形式来呈现:

[图片上传失败...(image-3f71a8-1642080911915)]

由于运行权限机制的出现,变得我们需要对新开发的应用去做适配,当然有人会问,那我之前开发的老应用不就完蛋了,是不是运行到6.0系统上就会发生各种崩溃?

其实不会的,谷歌做了良好的适配:
当你的应用targetSdkVersion小于23的时候,就算你运行在Android6.0系统上,它也会默认采用以前的权限管理机制,也就是一刀切。当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它才会采用新的这套权限管理机制。

所以如果你想逃开这个“麻烦”,只要把targetSdkVersion的版本设置为低于23就可以了,不过不建议采用这种方案。

当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,若不做适配,程序则会崩溃,报错信息如下:

01-16 02:23:46.181 E/CrashHandler Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE flg=0x3 cmp=com.android.camera/.Camera clip={text/uri-list U:file:///storage/emulated/0/teacher/head.jpg} (has extras) } from ProcessRecord{f7b731d 16568:com.lanxum.diagnosis.teacher.hg/u0a71} (pid=16568, uid=10071) with revoked permission android.permission.CAMERA
java.lang.SecurityException: Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE flg=0x3 cmp=com.android.camera/.Camera clip={text/uri-list U:file:///storage/emulated/0/teacher/head.jpg} (has extras) } from ProcessRecord{f7b731d 16568:com.lanxum.diagnosis.teacher.hg/u0a71} (pid=16568, uid=10071) with revoked permission android.permission.CAMERA

说了这么多,那么来看下怎么进行Android6.0(M)的权限管理适配吧,其实很简单,只需要记住下面几个API方法就可以:(API23之后提供,support-v4包下也有提供)

int checkSelfPermission(String permission) 用来检测应用是否已经具有权限
void requestPermissions(String[] permissions, int requestCode) 进行请求单个或多个权限
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 请求权限结果回调

[图片上传失败...(image-193991-1642080911916)]

以评测教师端app修改头像功能模块为例,该操作需要拍照和读写sd卡权限

下面来段代码示例(当用户点击照相机时)

//判断当前系统是否高于或等于6.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    //通过调用support-v4包提供的方法
    if (ContextCompat.checkSelfPermission(getActivity(), android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
        //具有拍照权限和读写SD卡权限,直接调用相机
        //具体调用代码
        takePhoto();
    } else {
        //不具有拍照权限和读写SD卡权限,需要进行权限申请;在Fragment中申请权限,不可以通过ActivityCompat.requestPermissions调用,直接使用Fragment的requestPermissions方法,否则会回调到Activity的onRequestPermissionsResult
        requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_CAMERA_CODE);
    }
} else {
    //当前系统小于6.0,直接调用拍照
    takePhoto();
}

private void takePhoto() {
    //照相
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    String externalStorageState = Environment.getExternalStorageState();
    if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
        File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
        if (!dir.exists()) dir.mkdir();
        File f = new File(dir, "head.jpg");
        Uri u = Uri.fromFile(f);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
        getActivity().startActivityForResult(intent, RESULT_CAPTURE);
    }
}

REQUEST_PERMISSION_CAMERA_CODE是个标识码,类似Intent跳转的REQUEST_CODE的,然后我们就可以在onRequestPermissionsResult进行权限申请的回调处理:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_PERMISSION_CAMERA_CODE) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.CAMERA) || !ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)) {
            ////如果用户选择了不再提示系统授权弹框,则自定义对话框引导用户进行授权
            showMissingPermissionDialog();
            return;
        }
        if (grantResults.length >= 2) {
            int cameraResult = grantResults[0];
            int storageResult = grantResults[1];
            if (cameraResult == PackageManager.PERMISSION_GRANTED && storageResult == PackageManager.PERMISSION_GRANTED) {
                takePhoto();
            }
        }
    }
}

private void showMissingPermissionDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle(R.string.help);
    builder.setMessage(R.string.string_help_text);

    // 关闭对话框
    builder.setNegativeButton(R.string.quit, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
        }
    });
    builder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            startAppSettings();
        }
    });
    builder.show();
}

// 启动应用的设置
private void startAppSettings() {
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:" + getActivity().getPackageName()));
    startActivity(intent);
}

[图片上传失败...(image-909967-1642080911916)]

注:shouldShowRequestPermissionRationale方法:当app完全没有机会被授权的时候,比如用户在权限对话框中选择了"不再显示”,调用shouldShowRequestPermissionRationale() 则返回false,这种情况下,我们可以通过自定义对话框引导用户进行授权,如下图

[图片上传失败...(image-30ea9-1642080911916)]

Android7.0适配

在Android7.0版本上,Android系统强制执行了StrictMode API 政策,禁止向你的应用外公开File://URI。如果一项包含文件File://URI类型的Intent离开你的应用,应用失败,并出现FileUriExposedException异常,比如系统相机拍照,裁剪照片

经测试,File api在应用内读取文件存储依然可以继续使用,应用间(主要指调用部分系统应用)进行共享会直接报错。由于评测教师端裁剪照片功能通过自定义的ClipHeaderActivity类来实现,属于应用内,所以不会报错

在Android7.0之前,如果你想调用系统相机拍照可以通过以下代码来进行:

    //照相
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    String externalStorageState = Environment.getExternalStorageState();
    if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
        File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
        if (!dir.exists()) dir.mkdir();
        File f = new File(dir, "head.jpg");
        Uri u = Uri.fromFile(f);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
        getActivity().startActivityForResult(intent, RESULT_CAPTURE);
    }   

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

使用FileProvider

使用FileProvider的大致步骤如下:

第一步:在manifest清单文件中注册provider

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${FILE_PROVIDER}"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths">
        </meta-data>
    </provider>

注:${FILE_PROVIDER}的值定义在主工程app目录下的build.gradle文件内,不同渠道的app需要配置不同的值,否则会和多渠道打包冲突

productFlavors {
    ningxia {
        applicationId "com.lanxum.diagnosis.teacher.nx"
        resValue "string", "app_name", "全优学教师-宁夏正式版"
        buildConfigField "String", "PREFIX_URL", "\"http://218.95.173.132\""
        buildConfigField "int", "VERSION", "2"
        buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.nx.takephoto.fileprovider\""
        manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.nx.takephoto.fileprovider"]
    }
    ningxiatest {
        applicationId "com.lanxum.diagnosis.teacher.nxtest"
        resValue "string", "app_name", "全优学教师-宁夏测试版"
        buildConfigField "String", "PREFIX_URL", "\"http://223.202.64.196:8084\""
        buildConfigField "int", "VERSION", "2"
        buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.nxtest.takephoto.fileprovider\""
        manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.nxtest.takephoto.fileprovider"]
    }
    huanggang {
        applicationId "com.lanxum.diagnosis.teacher.hg"
        resValue "string", "app_name", "全优学教师-黄冈版"
        buildConfigField "String", "PREFIX_URL", "\"http://221.235.188.44:51311\""
        buildConfigField "int", "VERSION", "1"
        buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.hg.takephoto.fileprovider\""
        manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.hg.takephoto.fileprovider"]
    }
}

第二步:指定共享的目录

为了指定共享的目录我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="camera_photos" />
</paths>
</resources>

<files-path/>代表的根目录: Context.getFilesDir()

<external-path/>代表的根目录: Environment.getExternalStorageDirectory()

<cache-path/>代表的根目录: getCacheDir()

注:上述代码中path="",是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path="pictures",
那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。

第三步:使用FileProvider

上述准备工作做完之后,现在我们就可以使用FileProvider了。
还是以调用系统相机拍照为例,我们需要将上述拍照代码修改为如下:

//照相

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
    File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
    if (!dir.exists()) dir.mkdir();
        File f = new File(dir, "head.jpg");
    Uri u;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        u = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER, f);
    } else {
        u = Uri.fromFile(f);
    }
    intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
    getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}

以上内容均为参考以下链接,上述示例代码我也只在模拟器上测试过,尚未在真机上测试,可能还存在不兼容和待优化的地方,想要了解更详细具体的内容,可以直接查看以下链接地址

参考链接:

http://www.jianshu.com/p/56b9fb319310

http://www.jianshu.com/p/a37f4827079a

http://www.jianshu.com/p/feb6dd8d2212

http://www.jianshu.com/p/d3a998ec04ad

https://www.aswifter.com/2015/11/04/android-6-permission/

http://www.chaisong.xyz/2016/11/20/2016-11-20/

https://my.oschina.net/u/990728/blog/549914

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容