Android SDK变更

SDK23 6.0版本

SDK版本targetSdkVersion>= 23时,有新的特性与注意事项

权限申请

在安卓6.0时,将不会在安装的时候授予权限.取而代之的是,App不得不在运行时一个一个去询问用户来授予权限。
权限分为两类:普通权限和危险权限
危险权限以分组的形式给出,同一组的任何一个权限被授予,其他权限也被授予。分组包括日历,相机,联系人,位置,麦克风,通话,传感器,短信,储存卡.

  1. 检查是否有权限。没有就去申请
if(ActivityCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE)
                != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CALL_PHONE},
                    PERMISSIONS_REQUEST_CALL_PHONE);
        }else {
            Intent intent = new Intent(Intent.ACTION_CALL);
            Uri data = Uri.parse("tel:"+"10086");
            intent.setData(data);
            startActivity(intent);
        }
  1. 权限申请的回调返回 onRequestPermissionsResult
@Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if(requestCode == PERMISSIONS_REQUEST_CALL_PHONE){
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED){
                Intent intent = new Intent(Intent.ACTION_CALL);
                Uri data = Uri.parse("tel:"+"10086");
                intent.setData(data);
                try {
                    startActivity(intent);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }else {
                Toast.makeText(this,"权限被拒绝",Toast.LENGTH_SHORT).show();
            }
        }
    }
  1. 处理不再询问选项。当用户选择了不再询问时,可以引导用户跳转到系统设置中,修改权限。
   //shouldShowRequestPermissionRationale 不再询问时 返回false
if(!ActivityCompat.shouldShowRequestPermissionRationale(this,permissions[0])) {
                    new AlertDialog.Builder(this).setMessage("需要电话权限,请到设置中打开")
                            .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    Intent intent = new Intent();
                                    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                                    Uri uri = Uri.fromParts("package", MainActivity.this.getPackageName(), null);
                                    intent.setData(uri);
                                    MainActivity.this.startActivity(intent);
                                }
                            }).show();

                }
fragment

另外,fragment中申请权限不需要再去用ActivityCompat.requestPermissions,直接用fragment的requestPermissions()方法

指纹识别

谷歌提供了指纹识别技术,旨在统一指纹识别的方案

  1. 申请权限
   <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
  1. 首先我们判断手机是否支持指纹识别,是否有相关的传感器,是否录入了相关指纹,然后才开始对指纹做出系列的操作
fingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE);
if (!fingerprintManager.isHardwareDetected()) {
                    //是否支持指纹识别
                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setMessage("没有传感器");
                    builder.setCancelable(true);
                    builder.create().show();
                } else if (!fingerprintManager.hasEnrolledFingerprints()) {
                    //是否已注册指纹
                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setMessage("没有注册指纹");
                    builder.setCancelable(true);
                    builder.create().show();
                } else {
                    Toast.makeText(MainActivity.this,"后续操作",Toast.LENGTH_SHORT).show();
                }
  1. 启动指纹识别需要以下参数
2018-09-04_104657.png
  • FingerprintManager.CryptoObject这是一个加密的对象类,用来保证认证的安全性 ,需要最后生成的cipher
    private static final String DEFAULT_KEY_NAME = "default_key";

    KeyStore keyStore;
    @RequiresApi(api = Build.VERSION_CODES.M)
    private void initKey() {
        try {
            keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);
            KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(DEFAULT_KEY_NAME,
                    KeyProperties.PURPOSE_ENCRYPT |
                            KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(true)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
            keyGenerator.init(builder.build());
            keyGenerator.generateKey();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // Cipher 此类为加密和解密提供密码功能
    @TargetApi(23)
    private void initCipher() {
        try {
            SecretKey key = (SecretKey) keyStore.getKey(DEFAULT_KEY_NAME, null);
            Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                    + KeyProperties.BLOCK_MODE_CBC + "/"
                    + KeyProperties.ENCRYPTION_PADDING_PKCS7);
            cipher.init(Cipher.ENCRYPT_MODE, key);
           // showFingerPrintDialog(cipher);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
  • 创建一个handler,用来处理回调的结果
private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 1:
                    Toast.makeText(MainActivity.this,"多次识别失败,并且,不能短时间内调用指纹验证",Toast.LENGTH_SHORT).show();
                    break;
                case 2:
                    Toast.makeText(MainActivity.this,"出错",Toast.LENGTH_SHORT).show();
                    break;
                case 3:
                    Toast.makeText(MainActivity.this,"成功",Toast.LENGTH_SHORT).show();
                    break;
                case 4:
                    Toast.makeText(MainActivity.this,"识别失败",Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };
  • 创建AuthenticationCallback的子类
private class FingerCallBack extends FingerprintManagerCompat.AuthenticationCallback{
        //多次识别失败,并且,不能短时间内调用指纹验证
        @Override
        public void onAuthenticationError(int errMsgId, CharSequence errString) {
            super.onAuthenticationError(errMsgId, errString);
            mHandler.obtainMessage(1, errMsgId, 0).sendToTarget();
        }

        //出错可恢复
        @Override
        public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
            super.onAuthenticationHelp(helpMsgId, helpString);
            mHandler.obtainMessage(2, helpMsgId, 0).sendToTarget();
        }

        //识别成功
        @Override
        public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
            super.onAuthenticationSucceeded(result);
            mHandler.obtainMessage(3).sendToTarget();
        }

        //识别失败
        @Override
        public void onAuthenticationFailed() {
            super.onAuthenticationFailed();
            mHandler.obtainMessage(4).sendToTarget();
            mImageView.startAnimation(mAnimation);
        }
    }

另外可以增加一个抖动的动画,来提示识别错误

        mAnimation=new TranslateAnimation(0,5,0,0);
        mAnimation.setDuration(800);
        mAnimation.setInterpolator(new CycleInterpolator(8));

可参考Android指纹识别API讲解,一种更快更好的用户体验,另外FingerprintManager在最新的Android 9.0系统上将会被取代,切换到改用 BiometricPrompt

SDK24 7.0版本

多窗口

长按OverView按钮,就能进入多窗口模式。默认支持多窗口。
禁用多窗口?

<application
  android:resizeableActivity="false">
</application>

再次长按OverView按钮后,会提示应用不支持分屏

快速回复

支持 通知栏直接回复的功能

  1. 创建个普通通知
   Notification builder = new Notification.Builder(MainActivity.this)
                        .setSmallIcon(R.mipmap.ic_launcher_round)
                        .setContentText("今天晚上一起玩")
                        .setContentTitle("来自 老王")
                        .setAutoCancel(true)
                        .setAction(action)
                        .builder();
  1. 创建快速回复的动作,并添加remoteInut
   RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY).setLabel("请回复").build();
   //KEY_TEXT_REPLY后续根据他来获取输入内容

   Intent intent = new Intent();
   intent.setAction("quick.reply.input");
   PendingIntent pendingIntent = PendingIntent.getBroadcast(MainActivity.this,0,intent,
                        PendingIntent.FLAG_ONE_SHOT);
   Notification.Action action = new Notification.Action.Builder(
                null, 
                "回复", pendingIntent)
                .addRemoteInput(remoteInput)
                .build();
2018-09-04_104926.png
  1. 创建广播接收消息
        IntentFilter filter = new IntentFilter();
        filter.addCategory(this.getPackageName());
        filter.addAction("quick.reply.input");
        registerReceiver(br, filter);
        nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    BroadcastReceiver br = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Bundle results = RemoteInput.getResultsFromIntent(intent);
            if (results != null) {
                CharSequence result = results.getCharSequence(KEY_TEXT_REPLY);
                if (TextUtils.isEmpty(result)) {
                    ((TextView) findViewById(R.id.tv)).setText("no content");
                } else {
                    ((TextView) findViewById(R.id.tv)).setText(result);
                }
            }
            nm.cancelAll();
            unregisterReceiver(this);
        }
    };
  1. 发送消息
   nm.notify(NOTIFICATION_ID,builder);

JIT/AOT 编译

在 Android N 中,我们添加了 Just in Time (JIT) 编译器,对 ART 进行代码分析,让它可以在应用运行时持续提升 Android 应用的性能。 JIT 编译器对 Android 运行组件当前的 Ahead of Time (AOT) 编译器进行了补充,有助于提升运行时性能,节省存储空间,加快应用更新和系统更新速度。

个人资料指导的编译让 Android 运行组件能够根据应用的实际使用以及设备上的情况管理每个应用的 AOT/JIT 编译。 例如,Android 运行组件维护每个应用的热方法的个人资料,并且可以预编译和缓存这些方法以实现最佳性能。 对于应用的其他部分,在实际使用之前不会进行编译。

除提升应用的关键部分的性能外,个人资料指导的编译还有助于减少整个 RAM 占用,包括关联的二进制文件。 此功能对于低内存设备非常尤其重要。

Android 运行组件在管理个人资料指导的编译时,可最大程度降低对设备电池的影响。 仅当设备处于空闲状态和充电时才进行编译,从而可以通过提前执行该工作节约时间和省电。

StrictMode API 政策

在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。

参考Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

有很不科学的适配方式是在Application的onCreate中加入

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
         StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
         StrictMode.setVmPolicy(builder.build());
         builder.detectFileUriExposure();
   }

正确的办法

使用FileProvider兼容

FileProvider实际上是ContentProvider的一个子类,它的作用也比较明显了,file:///Uri不给用,那么换个Uri为content://来替代。

  1. 声明provider
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.zhy.android7.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />   //需要设置一个meta-data,里面指向一个xml文件
</provider>
  1. 编写file_paths.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="" />   代表根目录
    <files-path name="files" path="" />    getFilesDir()
    <cache-path name="cache" path="" /> getCacheDir()
    <external-path name="external" path="" />  SD卡目录
    <external-files-path name="name" path="path" /> getExternalFilesDirs()
     <external-cache-path name="name" path="path" /> getExternalCacheDirs()
</paths>

每个节点有name跟path属性
通过一个虚拟的地址对文件地址进行映射
通过path以及xml节点确定可访问的目录,通过name属性来映射真实的文件路径。
path即为代表目录下的子目录,比如

<external-path
        name="external"
        path="pics" />  

文件名为:file://Environment.getExternalStorageDirectory()/pics/20170601-041411.png
生成出来的uri: content://com.zhy.android7.fileprovider/external/20170601-041411.png

在FileProvider的内部

public void attachInfo(Context context, ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

确定了exported必须是false,grantUriPermissions必须是true
所以在低版本系统中,FileProvider仅仅只被当做普通的Provider,而我们没有授权,所以会抛出throw new SecurityException("Provider must grant uri permissions")异常
所以需要去给其他应用进行授权,授权需要包名。很多时候,比如分享,我们并不知道最终用户会选择哪个app,所以我们可以这样

List<ResolveInfo> resInfoList = context.getPackageManager()
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    context.grantUriPermission(packageName, uri, flag);
}
根据Intent查询出的所以符合的应用,都给他们授权
在不需要的时候通过revokeUriPermission移除权限
快速完成适配
<application>
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.android7.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

file_paths.xml:
<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_file_path"
        path="" />
    <external-cache-path
        name="external_cache_path"
        path="" />

</paths>

最后 工具类
public class FileProvider7 {

    public static Uri getUriForFile(Context context, File file) {
        Uri fileUri = null;
        if (Build.VERSION.SDK_INT >= 24) {
            fileUri = getUriForFile24(context, file);
        } else {
            fileUri = Uri.fromFile(file);
        }
        return fileUri;
    }

    public static Uri getUriForFile24(Context context, File file) {
        Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
                context.getPackageName() + ".android7.fileprovider",
                file);
        return fileUri;
    }


    public static void setIntentDataAndType(Context context,
                                            Intent intent,
                                            String type,
                                            File file,
                                            boolean writeAble) {
        if (Build.VERSION.SDK_INT >= 24) {
            intent.setDataAndType(getUriForFile(context, file), type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (writeAble) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
        } else {
            intent.setDataAndType(Uri.fromFile(file), type);
        }
    }
}

v2签名

7.0以后,引入v2签名方式,防止apk文件被篡改,v2签名比普通的zip文件多一个签名区块。如果其他三个区块被修改,都会验证失败。所以v2比v1更安全

SDK 26 8.0版本

通知渠道

新增了通知渠道,用户可以根据渠道来屏蔽一些不想要的通知。但是目前各大手机厂商也未完全适配好

// 创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //创建通知渠道
    CharSequence name = "渠道名称1";
    String description = "渠道描述1";
    String channelId="channelId1";//渠道id     
    int importance = NotificationManager.IMPORTANCE_DEFAULT;//重要性级别
    NotificationChannel mChannel = new NotificationChannel(channelId, name, importance);
    mChannel.setDescription(description);//渠道描述
    mChannel.enableLights(true);//是否显示通知指示灯
    mChannel.enableVibration(true);//是否振动

    NotificationManager notificationManager = (NotificationManager) getSystemService(
            NOTIFICATION_SERVICE);
    notificationManager.createNotificationChannel(mChannel);//创建通知渠道
}
--------------------- 

安装未知apk

添加安装未知apk的权限

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

canRequestPackageInstalls查询是否有权限,然后添加跳转至安装未知应用的授权页面的代码。类似这样的页面

elma

private void installAPK(){

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
        if (hasInstallPermission) {
            //安装应用
        } else {
            //跳转至“安装未知应用”权限界面,引导用户开启权限
            Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
            Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
            startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
        }
    }else {
        //安装应用
    }

}

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

后台服务

Android 8.0 有一项复杂功能;系统不允许后台应用创建后台服务,会抛出Not allowed to start service Intent的异常。 因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。
在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知
如果应用在此时间限制内未调用 startForeground(),则系统将停止服务并声明此应用为 ANR

广播限制

Android O执行了更为严格的广播限制

  1. 动态注册的receiver,可接收任何显式和隐式广播
  2. 静态注册的receiver将不能收到隐式广播,但可以收到显式广播。
    也就是说xml中注册的广播 不再能通过IntentFilter中的action进行匹配,只能指定具体的receiverName
接收不到:
Intent intent = new Intent();
intent.setAction("*****.action");
sendBroadcast(intent);
--------------------- 
能接收:
Intent intent = new Intent();
intent.setClassName("****", "****");
sendBroadcast(intent);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 今天去了朱家角,未抱期待。但人山人海,还是让我不适应。一路上都是小店,放着一些不知道多久的食物,叫卖着。逛了一会儿...
    xxwade阅读 213评论 0 0
  • 这是我自己的故事,所以要从我来北京之前开始说起… 与其他北漂不太一样,我作为北漂具有得天独厚的地理优势——我的家乡...
    SlyviaXi阅读 233评论 0 0
  • 初遇,红墙黄叶,遍地落叶,漫天的秋色,叶叶都是相遇的喜悦,是那样的一个晚秋。她携清风而来,不染尘埃。他披一肩诗情而...
    一生如燕阅读 869评论 0 0