Android N系列适配---FileProvider
Android 7.0的适配,主要包含方面:
- Android 7.0 主要功能的diff---介绍主要Android7.0功能以及行为变更
- Android 7.0 最重要的一环适配---FileProvider的适配
- Android 7.0 对常规三方的影响---UIL为例
Android 7.0 功能diff---详细介绍Android7.0拥有的功能
- 多窗口支持:
- 用户可以一次在屏幕上打开两个应用,或者处于分屏模式时一个应用位于另一个应用之上。 用户可以通过拖动两个应用之间的分隔线来调整应用。
- 在 Android TV 设备上,应用可以将自身置于画中画模式,从而让它们可以在用户浏览或与其他应用交互时继续显示内容。
- 可以指定app Activity大小,防止用户调整到该尺寸以下
- 通知增强功能:
- 模板更新
- 少量代码调整,即可使用新的通知模版开发
- 消息样式更新
- MessageStyle 类,可配置消息,会话标题,以及内容视图
- 捆绑通知
- 系统可以将消息按一定规律给组合,如消息主题,用户可以适当的进行Dismiss和Archive等操作
- 直接回复
- 即时通讯应用,支持用户直接在通知界面中快速回复消息
- 自定义视图
- 两个新的 API,使用自定义视图时可以充分利用系统装饰元素,如通知标题和操作
- 模板更新
- Project Svelte 后台优化:
- 删除了三个常用隐式广播,继续扩展 JobScheduler 和 GCMNetworkManager
- apk signature scheme V2
- 新的应用签名方案
- Android Studio 2.2 和 Android Gradle 2.2 插件会使用 APK
- 附上官方链接:
https://developer.android.com/about/versions/nougat/android-7.0.html#multi-window_support
行为变更和影响
- 当设备处于低电耗,首先会限制,关闭应用网络访问,推迟作业和同步,一定时间后,会对除去PowerManager.WakeLock和Alarmmanager闹铃,GPS和WIFI扫描以外的进行低电耗限制
- 后台优化,删除了三个隐式广播,如果app用到了,需要及时的解除关系
- 应用间共享文件的修改
- 无障碍改进,屏幕缩放,设置向导中视觉设置
- 附上官方链接:
https://developer.android.com/about/versions/nougat/android-7.0-changes.html
Android 7.0 FileProvider的适配
- 是什么
- 关于安卓7.0的适配,其中变更最大的就是FileProvider,关于FileProvider并不是最新出来的东西,而是以前就已经存在,由于Android的安全机制 ,一个进程默认不能影响另外一个进程的,如读取私有数据 。 那么对于进程间的文件的共享 ,出于安全考虑,用FileProvider。FileProvider会基于manifest中的定义定义的一个xml文件(xml目录 下),为所有定义的文件生成content URIs,这样外部的应用在没有权限的情况下,可以通过授予临时权限的content uri,读取相应的文件。
FileProvider是v4 support中的类 , 就继承ContentProvider。也就是说content:// Uri 代替了 file:/// Uri. 在Android7.0时候,为了安全,谷歌把它作为了一个强制使用而已。针对file://URI,需要通过FileProvider来转换成content://URI进行访问。
- 关于安卓7.0的适配,其中变更最大的就是FileProvider,关于FileProvider并不是最新出来的东西,而是以前就已经存在,由于Android的安全机制 ,一个进程默认不能影响另外一个进程的,如读取私有数据 。 那么对于进程间的文件的共享 ,出于安全考虑,用FileProvider。FileProvider会基于manifest中的定义定义的一个xml文件(xml目录 下),为所有定义的文件生成content URIs,这样外部的应用在没有权限的情况下,可以通过授予临时权限的content uri,读取相应的文件。
- 限制
- 那么会有人要问,是否所有需要从本地存储的东西都会被限制呢,其实不然,谷歌做这项规定主要是针对,包含文件 URI 的 Intent 离开你的应用,换句话说,如果你的Intent中用到了Uri,这个时候你就需要提防一下了,比如说,你使用到了图片裁剪等功能。
- 怎么做
-
第一步:
- 全局找出项目中,需要修改的地方,如下:
- Uri.parse、Uri.fromFile、file://、content://、Context.getFilesDir()、Environment.getExternalStorageDirectory()、getCacheDir()以及最终要的intent.setDataAndType(为什么需要找这个,因为这个会携带uri进行传递,这个是重头戏)
-
第二步:
- 找到罪魁祸首之后,需要按照步骤适配了,依次顺序是,清单文件的修改,资源文件的修改,以及Java代码中的修改
-
第三步:
-
清单文件的修改---清单文件中,添加provider标签即可
<provider android:exported="false" android:grantUriPermissions="true" android:authorities="com.***.fileprovider" android:name="android.support.v4.content.FileProvider"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" ></meta-data> </provider>
-
-
第四步:
-
创建res/xml/filepaths.xml文件
< paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path path="" name="external-path" /> <files-path path="" name="files_path" /> <cache-path path="" name="cache-path" /> </paths>
在这个文件中,为每个目录添加一个XML元素指定目录。paths 可以添加多个子路径:< files-path> 分享app内部的存储;< external-path> 分享外部的存储;< cache-path> 分享内部缓存目录。
< files-path >
代表目录为:Context.getFilesDir()<external-path>
代表目录为:Environment.getExternalStorageDirectory()<cache-path>
代表目录为:getCacheDir()那么又存在了一个问题,国内由于rom众多,会产生各种路径,比如华为的/system/media/,以及外置sdcard,像此类路径该如何适配呢?
< root-path path="" name="root-path" />
在这里又有人要问了,为什么要加root_path就管用,下面我们就一起再追踪一下源码
-
我们打开FileProvider的源码
public class FileProvider extends ContentProvider
-
开篇就能看见几个变量
private static final String TAG_ROOT_PATH = "root-path"; private static final String TAG_FILES_PATH = "files-path"; private static final String TAG_CACHE_PATH = "cache-path"; private static final String TAG_EXTERNAL = "external-path";
-
里面有个重要方法parsePathStrategy,从xml我们定义临时授权的路径file_paths.xml中,解析以及对比路径
while ((type = in.next()) != END_DOCUMENT) { if (type == START_TAG) { final String tag = in.getName(); final String name = in.getAttributeValue(null, ATTR_NAME); String path = in.getAttributeValue(null, ATTR_PATH); File target = null; if (TAG_ROOT_PATH.equals(tag)) { target = buildPath(DEVICE_ROOT, path); } else if (TAG_FILES_PATH.equals(tag)) { target = buildPath(context.getFilesDir(), path); } else if (TAG_CACHE_PATH.equals(tag)) { target = buildPath(context.getCacheDir(), path); } else if (TAG_EXTERNAL.equals(tag)) { target = buildPath(Environment.getExternalStorageDirectory(), path); } if (target != null) { strat.addRoot(name, target); } } }
-
buildPath(DEVICE_ROOT, path)这个方法甚是晃眼
private static final File DEVICE_ROOT = new File("/");
-
到这里,我们应该就明白了,这个root代表的是根路径,如果还不明白,我们可以进入adb试一下
MacBook-Pro:~ baidu$ adb shell bullhead:/ $ cd / bullhead:/ $ ls
-
然后出现的路径是
acct config dev mnt property_contexts sbin sys cache d etc oem res sdcard system charger data firmware proc root storage vendor
然后我们就看到了熟悉的system 以及sdcard等,到这里我们就彻底明白,root_path是为我们的根路径进行了临时授权,如果要访问系统system以及外置sdcard的话,在这里将得到授权。
那么又有个问题,如果我写了root_path的话,其他的file_path等是不是就不用写了呢,答案是可以的,已经试验,确实可以。不过反过来想,如果每次都对根路径进行授权,那么这个FileProvider是不是意义就不大了呢,相当于安全性还是没有防护,所以,谷歌的良苦用心,我们还需要理解,大家授权的时候,还是要把所有的路径,能详细的,尽量详细一下。
附:至于为何path="",这里要写空,原因是空表示根目录都可以进行查找,当然如果路径确定,可以写成path="images/",这表示直接适配了images这个文件夹,也就是可以在这个文件夹下查找,而在这个文件夹外,照旧会报错。后面尾随的这个name,则可以随意写,当FileProvider转换路径的时候,就会用此name代替,比如
content://com.***.fileprovider/myimages/default_image.jpg
-
-
第五步:
- 在java代码中使用
//得到缓存路径的Uri Uri contentUri = FileProvider.getUriForFile(getActivity(), "com.***.fileprovider", file); //获取壁纸 Intent intent = WallpaperManager.getInstance(getActivity()).getCropAndSetWallpaperIntent(contentUri); //开启一个Activity显示图片,可以将图片设置为壁纸。调用的是系统的壁纸管理。 getActivity().startActivityForResult(intent, ViewerActivity.REQUEST_CODE_SET_WALLPAPER);
这样是否大功告成???
-
java中使用,需要的权限,intent携带的读写权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
你以为这样就真的完事儿了?
-
在适配过程中,发现有时候addFlag并不能完全的拥有权限,需要grantUriPermission获取权限
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
-
附上福利工具类
/** * Android N 适配工具类 */ public class NougatTools { /** * 将普通uri转化成适应7.0的content://形式 针对文件格式 * * @param context 上下文 * @param file 文件路径 * @param intent intent * @param type 图片或者文件,0表示图片,1表示文件 * @param intentType intent.setDataAndType * @return */ public static Intent formatFileProviderIntent( Context context, File file, Intent intent, String intentType) { Uri uri = FileProvider.getUriForFile(context, GlobalDef.nougatFileProvider, file); // 表示文件类型 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.setDataAndType(uri, intentType); return intent; } /** * 将普通uri转化成适应7.0的content://形式 针对图片格式 * * @param context 上下文 * @param file 文件路径 * @param intent intent * @param intentType intent.setDataAndType * @return */ public static Intent formatFileProviderPicIntent( Context context, File file, Intent intent) { Uri uri = FileProvider.getUriForFile(context, GlobalDef.nougatFileProvider, file); List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities( intent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } // 表示图片类型 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); return intent; } /** * 将普通uri转化成适应7.0的content://形式 * * @return */ public static Uri formatFileProviderUri(Context context, File file) { Uri uri = FileProvider.getUriForFile(context, GlobalDef.nougatFileProvider, file); return uri; } }
-
Android 7.0对三方工具的影响
UIL(Universal-Image-Loader)为例
关于imageloader适配,加载了本地图片,竟然没有问题
final ImageView imageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.view_banner, null);
String imageUri = "/mnt/sdcard/image.png";
ImageLoader.getInstance().displayImage("file://"+imageUri, imageView);
-
如果想找到为何没有影响,需要读imageloader源码,直接从imageloader中的加载图片displayImage方法入手
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
-
找到bmp != null && !bmp.isRecycled()判断,如果没有从本地找到或者被回收掉了的话,直接走LoadAndDisplayImageTask,去加载图片
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey); if (bmp != null && !bmp.isRecycled()) { L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); if (options.shouldPostProcess()) { ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri)); ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo, defineHandler(options)); if (options.isSyncLoading()) { displayTask.run(); } else { engine.submit(displayTask); } } else { options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); } } else { if (options.shouldShowImageOnLoading()) { imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources)); } else if (options.isResetViewBeforeLoading()) { imageAware.setImageDrawable(null); } ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri)); LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, defineHandler(options)); if (options.isSyncLoading()) { displayTask.run(); } else { engine.submit(displayTask); } }
-
在LoadAndDisplayImageTask的run方法中,会判断是否bitmap为空,这样的话,就会尝试load Bitmap
if (bmp == null || bmp.isRecycled()) { bmp = tryLoadBitmap();
-
这里才是加载图片的关键,首先去判断磁盘是否存在图片,如果存在,则直接从磁盘加载图片,如果本地没有,则取网络获取图片。
private Bitmap tryLoadBitmap() throws TaskCancelledException { Bitmap bitmap = null; try { File imageFile = configuration.diskCache.get(uri); if (imageFile != null && imageFile.exists() && imageFile.length() > 0) { L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey); loadedFrom = LoadedFrom.DISC_CACHE; checkTaskNotActual(); bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath())); } if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey); loadedFrom = LoadedFrom.NETWORK; String imageUriForDecoding = uri; if (options.isCacheOnDisk() && tryCacheImageOnDisk()) { imageFile = configuration.diskCache.get(uri); if (imageFile != null) { imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath()); } } checkTaskNotActual(); bitmap = decodeImage(imageUriForDecoding); if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { fireFailEvent(FailType.DECODING_ERROR, null); } } } catch (IllegalStateException e) { fireFailEvent(FailType.NETWORK_DENIED, null); } catch (TaskCancelledException e) { throw e; } catch (IOException e) { L.e(e); fireFailEvent(FailType.IO_ERROR, e); } catch (OutOfMemoryError e) { L.e(e); fireFailEvent(FailType.OUT_OF_MEMORY, e); } catch (Throwable e) { L.e(e); fireFailEvent(FailType.UNKNOWN, e); } return bitmap; }
-
首次进入肯定是bitmap是空的,找到tryCacheImageOnDisk方法
private boolean tryCacheImageOnDisk() throws TaskCancelledException { L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey); boolean loaded; try { loaded = downloadImage(); if (loaded) { int width = configuration.maxImageWidthForDiskCache; int height = configuration.maxImageHeightForDiskCache; if (width > 0 || height > 0) { L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey); resizeAndSaveImage(width, height); // TODO : process boolean result } } } catch (IOException e) { L.e(e); loaded = false; } return loaded; }
-
里面清晰的可以看见,有个downloadImage方法
private boolean downloadImage() throws IOException { InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader()); if (is == null) { L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey); return false; } else { try { return configuration.diskCache.save(uri, is, this); } finally { IoUtils.closeSilently(is); } } }
downloadImage方法中,获取到了一个Downloader,通过uri获取流
-
看看downloader是啥,有个子类BaseImageDownloader,看里面的getStream方法
public InputStream getStream(String imageUri, Object extra) throws IOException { switch (Scheme.ofUri(imageUri)) { case HTTP: case HTTPS: return getStreamFromNetwork(imageUri, extra); case FILE: return getStreamFromFile(imageUri, extra); case CONTENT: return getStreamFromContent(imageUri, extra); case ASSETS: return getStreamFromAssets(imageUri, extra); case DRAWABLE: return getStreamFromDrawable(imageUri, extra); case UNKNOWN: default: return getStreamFromOtherSource(imageUri, extra); } }
-
那么问题就来了,我们传入的是file://前缀,会最终到downloader中获取stream,继续看看getStreamFromFile
protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException { String filePath = Scheme.FILE.crop(imageUri); if (isVideoFileUri(imageUri)) { return getVideoThumbnailStream(filePath); } else { BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE); return new ContentLengthInputStream(imageStream, (int) new File(filePath).length()); } }
-
显而易见,crop方法有问题
public String crop(String uri) { if (!belongsTo(uri)) { throw new IllegalArgumentException(String.format("URI [%1$s] doesn't have expected scheme [%2$s]", uri, scheme)); } return uri.substring(uriPrefix.length()); }
-
uri.substring,有点意思,从uriPrefix的长度开始截取
Scheme(String scheme) { this.scheme = scheme; uriPrefix = scheme + "://"; }
这样就很明白了,UIL这个框架,直接从"file:// "往后,把具体的地址截取出来了,而且它直接用后面的地址获取到了InputStream,这样就可以避免7.0这个file://需要换成content://的问题,而避免了使用FileProvider。
-
最后附上一个没问题的例子。
FileInputStream fileInputStream = new FileInputStream("/storage/emulated/0/Download/com.***.apk");