一、权限更改
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。也就是说,对于应用间共享文件这块,Android N中做了强制性要求
来看一段代码
String cachePath = getApplicationContext().getExternalCacheDir().getPath();
File picFile = new File(cachePath, "test.jpg");
Uri picUri = Uri.fromFile(picFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, picUri);
startActivityForResult(intent, 100);
这是常见的打开系统相机拍照的代码,拍照成功后,照片会存储在picFile文件中。
这段代码在Android 7.0之前是没有任何问题,但是如果你尝试在7.0的系统上运行,会抛出FileUriExposedException异常。
使用FileProvider
FileProvider使用大概分为以下几个步骤:
- manifest中申明FileProvider
- res/xml中定义对外暴露的文件夹路径
- 生成content://类型的Uri
- 给Uri授予临时权限
- 使用Intent传递Uri
1.manifest中申明FileProvider:
...
...
...
android:name:provider你可以使用v4包提供的FileProvider,或者自定义的,只需要在name申明就好了,一般使用系统的就足够了。
android:authorities:类似schema,命名空间之类,后面会用到。
android:exported:false表示我们的provider不需要对外开放。
android:grantUriPermissions:申明为true,你才能获取临时共享权限。
2. res/xml中定义对外暴露的文件夹路径:
新建paths.xml,文件名随便起,后面会引用到。
name:一个引用字符串。
path:文件夹“相对路径”,完整路径取决于当前的标签类型。
path可以为空,表示指定目录下的所有文件、文件夹都可以被共享。
paths这个元素内可以包含以下一个或多个,具体如下:
物理路径相当于Context.getFilesDir() + /path/。
物理路径相当于Context.getCacheDir() + /path/。
物理路径相当于Environment.getExternalStorageDirectory() + /path/。
物理路径相当于Context.getExternalFilesDir(String) + /path/。
物理路径相当于Context.getExternalCacheDir() + /path/。
3.生成content://类型的Uri
我们通常通过File生成Uri的代码是这样:
File picFile = xxx;
Uri picUri = Uri.fromFile(picFile);
这样生成的Uri,路径格式为file://xxx。这种Uri是无法在App之间共享的,我们需要生成content://xxx类型的Uri,方法就是通过Context.getUriForFile来实现:
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(),
"com.demo.fileprovider", newFile)
4.给Uri授予临时权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
FLAG_GRANT_READ_URI_PERMISSION:表示读取权限;
FLAG_GRANT_WRITE_URI_PERMISSION:表示写入权限。
5.使用Intent传递Uri
以开头的拍照代码作为示例,需要这样改写:
File imagePath = new File(Context.getFilesDir(), "images");
if (!imagePath.exists()){imagePath.mkdirs();}
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(),
"com.mydomain.fileprovider", newFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
// 授予目录临时共享权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 100);
二、广播
于Android N后台的优化主要是关闭了三项系统广播:网络状态变更广播、拍照广播以及录像广播。
网络变化的广播(CONNECTIVITY_CHANGE),当网络发生变化时所有注册了隐式监听网络变化的app都会被启动。删除这些广播可以显著提升设备性能和用户体验。同样地,拍照广播和录视频广播(ACTION_NEW_PICTURE or ACTION_NEW_VIDEO)也会出现上述情况。
在Android N平台下即使在Manifest.xml清单文件中注册了 CONNECTIVITY_ACTION广播,在网络发生变化时也不会接收到任何的信息。但是正在前台运行的应用程序如果在主线程中通过Context.registerReceiver()动态注册了CONNECTIVITY_ACTION广播,该应用程序仍然可以接收到该广播。
三、分屏
Android N允许用户一次在屏幕中使用两个App,用户可以左右并排/上下摆放两个App来使用。用户还可以左右/上下拖拽中间的那个小白线来改变两个App的尺寸。
如何操作来进入分屏模式的:
- 点击右下角的方块,进入任务管理器,长按一个App的标题栏,将其拖入屏幕的高亮区域,这个App金进入了分屏模式。然后在任务管理器中选择另一个App,单击它使得这个App也进入分屏模式。
- 打开一个App,然后长按右下角的方块,此时已经打开的这个App将进入分屏模式。然后在屏幕上的任务管理器中选择另外一个App,单击它使得这个App也进入分屏模式。
分屏模式的生命周期
官方说法:在分屏模式下,用户最近操作、激活过的Activity将被系统视为topmost。而其他的Activity都属于paused状态,即使它是一个对用户可见的Activity。但是这些可见的处于paused状态的Activity将比那些不可见的处于paused状态的Activity得到更高优先级的响应。当用户在一个可见的paused状态的Activity上操作时,它将得到恢复resumed状态,并被系统视为topmost。而之前那个那个处于topmpst的Activity将变成paused状态。
那么这种可见的pause的状态将带来什么影响呢?
在分屏模式中,一个App可以在对用户可见的状态下进入paused状态,所以你的App在处理业务时,应该知道自己什么时候应该真正的暂停。例如一个视频播放器,如果进入了分屏模式,就不应该在onPaused()回调中暂停视频播放,而应该在onStop()回调中才暂停视频,然后在onStart回调中恢复视频播放。关于如果知道自己进入了分屏模式,在Android N的Activity类中,增加了一个void onMultiWindowChanged(boolean inMultiWindow)回调,所以我们可以在这个回调知道App是不是进入了分屏模式。
分屏时Activity的生命周期
- 当前显示自己的应用页面,长按多任务键时出现分屏
onConfigurationChanged()-> onMultiWindowModeChanged()-> onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()
- 分屏时长按多任务键,全屏显示自己的应用时
onPause()-> onStop()-> onDestroy()-> onCreate()-> onStart()-> onResume()-> onPause()-> onConfigurationChanged()-> onMultiWindowModeChanged()-> onResume()
如何设置App的分屏模式
怎样才能让App进入分屏模式呢?有下面这几个属性。
android:resizeableActivity
直接在AndroidManifest.xml中的
设置了这个属性后,你的App/Activity就可以进入分屏模式了。
如果这个属性被设为false,那么你的App将无法进入分屏模式,如果你在打开这个App时,长按右下角的小方块,App将仍然处于全屏模式,系统会弹出Toast提示你无法进入分屏模式。这个属性在你target到Android N后,android:resizeableActivity的默认值就是true。
注意:假如你没有适配到Android N(targetSDKVersion < Android N),打包App时的compileSDKVersion < Android N,你的App也是可以支持分屏的!!!!原因在于:如果你的App没有设置 仅允许Activity竖屏/横屏,即没有设置android:screenOrientation="XXX"属性时,运行Android N系统的设备还是可以将你的App分屏!! 但是这时候系统是不保证运行时的稳定性的,在进入分屏模式时,系统首先也会弹出Toast来提示你说明这个风险。
最新的Android N SDK中,Activity类中增加了下面的方法。
- inMultiWindow():返回值为boolean,调用此方法可以知道App是否处于分屏模式。
- onMultiWindowChanged(boolean inMultiWindow):当Activity进入或者退出分屏模式时,系统会回调这个方法来通知开发者。回调的参数inMultiWindow为boolean类型,如果inMultiWindow为true,表示Activity进入分屏模式;如果inMultiWindow为false,表示退出分屏模式。
支持拖拽
现在可以实现在两个分屏模式的Activity之间拖动内容。Android N Preview SDK中,View已经增加支持Activity之间拖动的API。具体的类和方法主要用到下面几个新的接口:
- View.startDragAndDrop():View.startDrag() 的替代方法,需要传递View.DRAG_FLAG_GLOBAL来实现跨Activity拖拽。如果需要将URI权限传递给接收方Activity,还可以根据需要设置View.DRAG_FLAG_GLOBAL_URI_READ或者View.DRAG_FLAG_GLOBAL_URI_WRITE。
- View.cancelDragAndDrop():由拖拽的发起方调用,取消当前进行中的拖拽。
- View.updateDragShadow():由拖拽的发起方调用,可以给当前进行的拖拽设置阴影。
- android.view.DropPermissions:接收方App所得到的权限列表。
- Activity.requestDropPermissions():传递URI权限时,需要调用这个方法。传递的内容存储在DragEvent中的ClipData里。返回值为前面的android.view.DropPermissions。
在FirstActivity中,发起拖拽。
imageView.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
/**
* 构造一个ClipData,将需要传递的数据放在里面
*/
ClipData.Item item = new ClipData.Item((CharSequence) view.getTag());
String[] mimeTypes = {ClipDescription.MIMETYPE_TEXT_PLAIN};
ClipData dragData = new ClipData(view.getTag().toString(), mimeTypes, item);
View.DragShadowBuilder shadow = new View.DragShadowBuilder(imageView);
/**
* startDragAndDrop是Android N SDK中的新方法,替代了以前的startDrag,
* flag需要设置为DRAG_FLAG_GLOBAL
*/
view.startDragAndDrop(dragData, shadow, null, View.DRAG_FLAG_GLOBAL);
return true;
} else {
return false;
}
}
});
在SecondActivity中,接收这个拖拽的结果,在ACTION_DROP事件中,把结果显示出来。
dropedText.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(View view, DragEvent dragEvent) {
switch (dragEvent.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
Log.d(TAG, "Action is DragEvent.ACTION_DRAG_STARTED");
break;
case DragEvent.ACTION_DRAG_ENTERED:
Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENTERED");
break;
case DragEvent.ACTION_DRAG_EXITED:
Log.d(TAG, "Action is DragEvent.ACTION_DRAG_EXITED");
break;
case DragEvent.ACTION_DRAG_LOCATION:
break;
case DragEvent.ACTION_DRAG_ENDED:
Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENDED");
break;
case DragEvent.ACTION_DROP:
Log.d(TAG, "ACTION_DROP event");
//在这里显示接收到的结果
dropedText.setText(dragEvent.getClipData().getItemAt(0).getText());
break;
default:
break;
}
return true;
}
});