AndroidQ 沙箱适配多媒体文件(小结)

综述

所有内容的访问变化见下图:

外部媒体文件的扫描,读取和写入

最容易被踩坑的应该是,对外部媒体文件,照片,视频,图片的读取或写入。

扫描

首先是扫描。扫描依然是使用 query MediaStore 的方式。一句话介绍 MediaStore,MediaStore 就是Android系统中的一个多媒体数据库。代码如下图所示,以搜索本地视频为例子:

protected List<VideoInfo> doInBackground(Void... params) {
  mContentResolver = context.getContentResolver();

  String[] mediaColumns = { MediaStore.Video.Media._ID, MediaStore.Video.Media.DATA,
      MediaStore.Video.Media.TITLE, MediaStore.Video.Media.MIME_TYPE,
      MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.SIZE,
      MediaStore.Video.Media.DATE_ADDED, MediaStore.Video.Media.DURATION,
      MediaStore.Video.Media.WIDTH, MediaStore.Video.Media.HEIGHT };

  Cursor mCursor = mContentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, mediaColumns,
      null, null, MediaStore.Video.Media.DATE_ADDED);

  if (mCursor == null) {
    return null;
  }

  // 注意,DATA 数据在 Android Q 以前代表了文件的路径,但在 Android Q上该路径无法被访问,因此没有意义。
  ixData = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA);
  ixMime = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE);
  // ID 是在 Android Q 上读取文件的关键字段
  ixId = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
  ixSize = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);
  ixTitle = mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE);

  allImages = new ArrayList<VideoInfo>();
  mTotalVideoCount = 0;

  mCursor.moveToLast();

  while (mCursor.moveToPrevious()) {
    if (addVideo(mCursor) == 0) {
      continue;
    } else if (addVideo(mCursor) == 1) {
      break;
    }
  }

  mCursor.close();

  return allImages;
}

既然 data 不可用,就需要知晓 id 的使用方式,首先是使用 id 拼装出 content uri ,如下所示:

public getRealPath(String id) {
  return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build().toString();
}

Image 同理换成 MediaStore.Images。

读取和写入

其次,是读取 content uri。这里需要注意 File file = new File(contentUri); 是无法获取到文件的。file.exist() 为 false。

那么就产生两个问题:1. 如何确定 ContentUri 形式的文件存在 2. 如何读取或写入文件。

首先,对于 Content Uri 的读取,必须借助于 ContentResolver。

其次,对于 1,没有找到 Google 文档中提供比较容易的API,只能采用打开 FileDescriptor 是否成功的形式,代码如下所示:

public boolean isContentUriExists(Context context, Uri uri) {
  if (null == context) {
    return false;
  }
  ContentResolver cr = context.getContentResolver();
  try {
    AssetFileDescriptor afd = cr.openAssetFileDescriptor(uri, "r");
    if (null == afd) {
      iterator.remove();
    } else {
      try {
        afd.close();
      } catch (IOException e) {
      }
    }
  } catch (FileNotFoundException e) {
    return false;
  }

  return true;
}

这种方法最大的问题即是,对应于一个同步 I/O 调用,易造成线程等待。因此,目前对于 MediaStore 中扫描出来的文件可能不存在的情况,没有直接的好方法可以解决过滤。

对于问题 2,如 1 所示,可以借助 Content Uri 从 ContentResolver 里面拿到 AssetFileDescriptor,然后就可以拿到 InputSteam 或 OutputStream,那么接下来的读取和写入就非常自然,如下所示:

public static void copy(File src, ParcelFileDescriptor parcelFileDescriptor) throws IOException {
  FileInputStream istream = new FileInputStream(src);
  try {
    FileOutputStream ostream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
    try {
      IOUtil.copy(istream, ostream);
    } finally {
      ostream.close();
    }
  } finally {
    istream.close();
  }
}

public static void copy(ParcelFileDescriptor parcelFileDescriptor, File dst) throws IOException {
  FileInputStream istream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
  try {
    FileOutputStream ostream = new FileOutputStream(dst);
    try {
      IOUtil.copy(istream, ostream);
    } finally {
      ostream.close();
    }
  } finally {
    istream.close();
  }
}

public static void copy(InputStream ist, OutputStream ost) throws IOException {
  byte[] buffer = new byte[4096];
  int byteCount = 0;
  while ((byteCount = ist.read(buffer)) != -1) { // 循环从输入流读取 buffer字节
    ost.write(buffer, 0, byteCount);    // 将读取的输入流写入到输出流
  }
}

保存媒体文件到公共区域

这里仅以 Video 示例,Image、Downloads 基本类似:

public static Uri insertVideoIntoMediaStore(Context context, String fileName) {
  ContentValues contentValues = new ContentValues();
  contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
  contentValues.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
  contentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");

  Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
  return uri;
}

这里所做的,只是往 MediaStore 里面插入一条新的记录,MediaStore 会返回给我们一个空的 Content Uri,接下来问题就转化为往这个 Content Uri 里面写入,那么应用上一节所述的代码即可实现。

Video 的 Thumbnail 问题

在 Android Q 上已经拿不到 Video 的 Thumbnail 路径了,又由于没有暴露 Video 的 Thumbnail 的 id ,导致了 Video 的 Thumbnail 只能使用实时获取 Bitmap 的方法,如下所示:

private Bitmap getThumbnail(ContentResolver cr, long videoId) throws Throwable {
  return MediaStore.Video.Thumbnails.getThumbnail(cr, videoId, MediaStore.Video.Thumbnails.MINI_KIND,
      null);
}

可以进去看 Android SDK 的实现,其中最关键的部分是:

String column = isVideo ? "video_id=" : "image_id=";
c = cr.query(baseUri, PROJECTION, column + origId, null, null);
if (c != null && c.moveToFirst()) {
  bitmap = getMiniThumbFromFile(c, baseUri, cr, options);
  if (bitmap != null) {
    return bitmap;
  }
}

进一步再进去看,可以发现直接就把 Video/Image 文件打开计算 Thumbnail。

private static Bitmap getMiniThumbFromFile(
    Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) {
  Bitmap bitmap = null;
  Uri thumbUri = null;
  try {
    long thumbId = c.getLong(0);
    String filePath = c.getString(1);
    thumbUri = ContentUris.withAppendedId(baseUri, thumbId);
    ParcelFileDescriptor pfdInput = cr.openFileDescriptor(thumbUri, "r");
    bitmap = BitmapFactory.decodeFileDescriptor(
        pfdInput.getFileDescriptor(), null, options);
    pfdInput.close();
  } catch (FileNotFoundException ex) {
    Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
  } catch (IOException ex) {
    Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
  } catch (OutOfMemoryError ex) {
    Log.e(TAG, "failed to allocate memory for thumbnail "
        + thumbUri + "; " + ex);
  }
  return bitmap;
}

这个 API 毫无疑问设计的非常不合理,没有暴露 Thumbnail 的系统缓存给开发者,造成了每次都要重新I/O 计算的极大耗时。强烈呼吁 Android Q 的正式版能修正这个 API 设计缺陷。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android Q适配之IMEI替换为Android_id

    前置工作: 项目配置升到对应的29版本 compileSdkVersion: 29, buildToolsVersion: '29.0.0', minSdkVersion : 19, targetSdkVersion : 29, javaVersion : JavaVersion.VERSION_1_8 升级到Android Q后的权限提示界面 老版本获取IMEI的方法: public static String getIMEI(Context context) { String deviceI

  • AndroidQ 沙箱适配多媒体文件(小结)

    综述 所有内容的访问变化见下图: 外部媒体文件的扫描,读取和写入 最容易被踩坑的应该是,对外部媒体文件,照片,视频,图片的读取或写入. 扫描 首先是扫描.扫描依然是使用 query MediaStore 的方式.一句话介绍 MediaStore,MediaStore 就是Android系统中的一个多媒体数据库.代码如下图所示,以搜索本地视频为例子: protected List<VideoInfo> doInBackground(Void... params) { mContentResolv

  • webpack的移动端适配方案小结

    目录 rem vw 适配第三方UI框架 结语 在移动端开发的过程中,一个最常见的问题就是适配不同的屏幕宽度.目前比较常见的适配方案有rem和vw,它们都是css中的相对单位. rem W3C对rem的定义是 font-size of the root element,它是一个只相对于浏览器的根元素(HTML元素)的font-size的来确定的单位,也就是说对于不同宽度的机型,我们只需要计算出对应的根元素的字体大小,用同样的css代码可以实现等比适配.考虑最简单的情况: html代码片段 //移动

  • iPhoneX 序列适配方案(小结)

    和往常一样,苹果发布新产品,我们作为开发者都需要对系统和UI布局进行适配,今年也是一样.从去年发布的 iphoneX开始,iPhone 手机加入了刘海设计,而且针对于iphone的刘海,需要特殊的适配.今年新出的3款iphone都带有刘海,自然也不例外. 在iphonex以前iphone的顶部导航栏高度都是统一的64,底部导航栏是统一的49:从iphonex的刘海屏开始,出了一个SafeArea的概念,带刘海设计的iphone,顶部导航的高度由原来的64,变成了88,因为状态栏的高度由原来的20

  • AndroidQ(10)分区存储完美适配方法

    前言 最近时间在做AndroidQ的适配,截止到今天AndroidQ分区存储适配完成,期间出现很多坑,目前网上的帖子大部分都是概述变更内容,接下来的几篇帖子都是对分区存储实际经验代码总结,填坑经验,特此记录一下,也为大家提供帮助. 本篇主要是对AndroidQ(10)分区存储适配具体实现 要点: Android Q文件存储机制修改成了沙盒模式 APP只能访问自己目录下的文件和公共媒体文件 对于AndroidQ以下,还是使用老的文件存储方式 这里需要注意:在适配AndroidQ的时候还要兼容Q系统

  • Android 10 适配攻略小结

    相比较去年写的Android 9适配,这次Android 10的内容有点多.没想到写了我整整两天,吐血中... 准备工作 老规矩,首先将我们项目中的 targetSdkVersion 改为 29. 1.Scoped Storage(分区存储) 说明 在Android 10之前的版本上,我们在做文件的操作时都会申请存储空间的读写权限.但是这些权限完全被滥用,造成的问题就是手机的存储空间中充斥着大量不明作用的文件,并且应用卸载后它也没有删除掉.为了解决这个问题,Android 10 中引入了 Sco

  • 适配AndroidQ拍照和读取相册图片的实现方法

    Google发行Android Q版本也有很长一段时间了,华为应用市场已经要求要适配Android Q版本了,所以,我们也要去对Android Q进行适配. 先讲一下咱们这节用到的新特性 Android Q文件存储机制修改成了沙盒模式,类似于iOS 应用只能访问自己沙盒下的文件和公共媒体文件 如果有想具体了解Android Q新版特效的可以去 官方文档 我们在这个地方记录一下Android Q版本进行拍照保存到相册的功能. 权限问题 Android Q不再需要申请文件读写权限,默认可以读写自己沙

  • AndroidQ分区存储权限变更及适配的实现

    分区存储 在Android Q中引入了分区储存功能,在外部存储设备中为每个应用提供了一个"隔离存储沙盒".其他应用无法直接访问应用的沙盒文件.由于文件是应用的私有文件,不再需要任何权限即可访问和保存自己的文件.此变更并有助于减少应用所需的权限数量,同时保证用户文件的隐私性. 权限变更 Android Q 更改了应用对设备外部存储设备中的文件(如:/sdcard )的访问方式.继续使用 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限,只

  • AndroidQ沙盒机制之分区存储适配

    为了让用户更好地控制自己的文件,Android Q更改了应用访问设备外部存储空间中文件的方式.Android Q用更精细的媒体特定权限来替换READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,并且无需特定权限,应用即可访问自己在外部存储设备的文件. 1.针对应用私有文件的隔离存储沙盒 对于每个应用,Android Q 都会创建一个"隔离存储沙盒",以限制其他应用访问本应用在外部存储设备的文件.常见的外部存储设备是/sdcard.此定义具有两个优

  • AndroidQ(10)黑暗模式适配的实现

    前言:作为一个Android程序员,每年最期待就是Google的发布会啦!!这不,今年的AndroidQ如期而至.这里简单介绍一下Android的新特性: AndroidQ全局暗黑模式 隐私权限的更新 AndroidQ新版的手势导航(其实就是仿IOS) 系统日程UI的优化(还有其他系统UI上的优化) Google组件(jetpack)的推荐 每年的Google大会一结束就是程序员忙碌工作的开端,各种适配,各种新功能- 一堆事情下来,搞的焦头烂额. 但是今年的发布会之后,仔细一看Q的更新清单,其实

  • iOS15适配小结

    目录 1.tabbar及navicationbar的背景颜色问题 原因:因为设置颜色方法在ios15中失效 解决方法--重新设置相关属性 2.tableview新属性-sectionHeaderTopPadding 使用 1.tabbar及navicationbar的背景颜色问题 问题:从ios14升级到ios15会出现 导航栏背景颜色失效 原因:因为设置颜色方法在ios15中失效 --在iOS13更新的API中新增了针对navigationBar,tabbar分别新增了新的属性专门管理这些滑动

随机推荐