Android SDK变更

2019-01-02  本文已影响0人  30cf443c3643

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
    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);
        }
    }
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;
            }
        }
    };
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);
上一篇下一篇

猜你喜欢

热点阅读