美文网首页Android TechAndroid nougat
Android N系列适配---FileProvider

Android N系列适配---FileProvider

作者: 25a58172fbb5 | 来源:发表于2016-10-31 16:13 被阅读3422次

    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拥有的功能

    1. 多窗口支持:
      1. 用户可以一次在屏幕上打开两个应用,或者处于分屏模式时一个应用位于另一个应用之上。 用户可以通过拖动两个应用之间的分隔线来调整应用。
      2. 在 Android TV 设备上,应用可以将自身置于画中画模式,从而让它们可以在用户浏览或与其他应用交互时继续显示内容。
      3. 可以指定app Activity大小,防止用户调整到该尺寸以下
    2. 通知增强功能:
      1. 模板更新
        1. 少量代码调整,即可使用新的通知模版开发
      2. 消息样式更新
        1. MessageStyle 类,可配置消息,会话标题,以及内容视图
      3. 捆绑通知
        1. 系统可以将消息按一定规律给组合,如消息主题,用户可以适当的进行Dismiss和Archive等操作
      4. 直接回复
        1. 即时通讯应用,支持用户直接在通知界面中快速回复消息
      5. 自定义视图
        1. 两个新的 API,使用自定义视图时可以充分利用系统装饰元素,如通知标题和操作
    3. Project Svelte 后台优化:
      1. 删除了三个常用隐式广播,继续扩展 JobScheduler 和 GCMNetworkManager
    4. apk signature scheme V2
      1. 新的应用签名方案
      2. Android Studio 2.2 和 Android Gradle 2.2 插件会使用 APK
    5. 附上官方链接:
      https://developer.android.com/about/versions/nougat/android-7.0.html#multi-window_support

    行为变更和影响
    1. 当设备处于低电耗,首先会限制,关闭应用网络访问,推迟作业和同步,一定时间后,会对除去PowerManager.WakeLock和Alarmmanager闹铃,GPS和WIFI扫描以外的进行低电耗限制
    2. 后台优化,删除了三个隐式广播,如果app用到了,需要及时的解除关系
    3. 应用间共享文件的修改
    4. 无障碍改进,屏幕缩放,设置向导中视觉设置
    5. 附上官方链接:
      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进行访问。
    • 限制
      • 那么会有人要问,是否所有需要从本地存储的东西都会被限制呢,其实不然,谷歌做这项规定主要是针对,包含文件 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);
    
    1. 如果想找到为何没有影响,需要读imageloader源码,直接从imageloader中的加载图片displayImage方法入手

       public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
           ImageLoadingListener listener, ImageLoadingProgressListener progressListener) 
      
    2. 找到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);
           }
       }
      
    3. 在LoadAndDisplayImageTask的run方法中,会判断是否bitmap为空,这样的话,就会尝试load Bitmap

       if (bmp == null || bmp.isRecycled()) {
               bmp = tryLoadBitmap();
      
    4. 这里才是加载图片的关键,首先去判断磁盘是否存在图片,如果存在,则直接从磁盘加载图片,如果本地没有,则取网络获取图片。

       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;
       }
      
    5. 首次进入肯定是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;
           }
      
    6. 里面清晰的可以看见,有个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);
               }
           }
       }
      
    7. downloadImage方法中,获取到了一个Downloader,通过uri获取流

    8. 看看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);
           }
       }
      
    9. 那么问题就来了,我们传入的是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());
           }
       }
      
    10. 显而易见,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());
          }
      
    11. uri.substring,有点意思,从uriPrefix的长度开始截取

      Scheme(String scheme) {
              this.scheme = scheme;
              uriPrefix = scheme + "://";
          }
      
    12. 这样就很明白了,UIL这个框架,直接从"file:// "往后,把具体的地址截取出来了,而且它直接用后面的地址获取到了InputStream,这样就可以避免7.0这个file://需要换成content://的问题,而避免了使用FileProvider。

    13. 最后附上一个没问题的例子。

      FileInputStream fileInputStream = new FileInputStream("/storage/emulated/0/Download/com.***.apk");

    相关文章

      网友评论

      • Wings6:创建xml文件的是 xml里面写什么内容??
        是写入这个吗???
        < paths xmlns:android="http://schemas.android.com/apk/res/android&quot;>
        <external-path path="" name="external-path" />
        <files-path path="" name="files_path" />
        <cache-path path="" name="cache-path" />
        </paths>


      • sugaryaruan:干货,必须点赞
      • hemingway1014:请教一个问题:是不是图片裁剪的时候传递uri不需要使用FileProvider就可以了?我在适配Android N的时候,拍照必须使用FileProvider,但是到裁剪的时候直接File:类型的uri就可以运行通过。
        starsight:我看确实是这样,自带的裁剪感觉有点问题呀

      本文标题:Android N系列适配---FileProvider

      本文链接:https://www.haomeiwen.com/subject/pmjputtx.html