综述

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

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

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

扫描

首先是扫描。扫描依然是使用 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 设计缺陷。

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

AndroidQ 沙箱适配多媒体文件(小结)的更多相关文章

  1. 如何分离iOS APNS通知的沙箱和生产设备令牌

    我不小心并在同一个db表中混合沙箱和生产设备令牌.它导致一些安装生产应用程序的设备无法接收推送通知.如何从db表中分离沙箱令牌和生产令牌?非常感谢您的帮助!!

  2. xcode – Mac App Store拒绝 – 未启用应用沙箱

    我已将我的应用程序提交到MacAppStore,并且验证正常.但是,我继续使用以下内容获取无效二进制消息;AppsandBoxnotenabled–Thefollowingexecutablesmustincludethe“com.apple.security.app-sandBox”entitlementwithaBooleanvalueoftrueintheentitlementsproper

  3. 如何在Xcode 4中编码和沙箱助手应用?

    这就是问题:我有一个包含HelperApp的MainApp.Helper应用程序用于登录项目,因此我需要区分MainApp和HelperApp软件包ID.由于BuildPhasecopy,我将HelperApp复制到MainApp中.如果我代码和沙箱HelperApp上传阶段停止…

  4. ios – instagram沙箱用户邀请已发送但用户在哪里接受?

    我正在使用Instagram并且能够将邀请发送给我的第二个用户.但是,当第二个用户登录此链接时:他们得到了{“code”:403,“error_type”:“OAuthForbiddenException”,“error_message”:“您不是此客户端的沙箱用户”}我还以该用户身份登录到官方Instagram应用程序,但我仍无法接受邀请.沙箱用户如何接受邀请才能使用我的应用?解决方法弄清楚了.

  5. swift2 – Playground Xcode:无法获得沙箱扩展

    我在IOS9xcode项目中的.playground文件中出现此错误消息:我所做的是:>开始了Xcode7/Swift2项目>使用“podinstallAlamofire”>使用以下代码创建.playground文件:进口Alamofire如何在.playground文件中测试此库而不会出错?根据业主的说法,从版本3开始,游乐场支持似乎已被删除:Weremovedtheplaygroundinth

  6. Vant+postcss-pxtorem 实现浏览器适配功能

    这篇文章主要介绍了Vant+postcss-pxtorem 实现浏览器适配,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  7. iOS10 适配以及Xcode8配置总结

    这篇文章主要介绍了iOS10 适配以及Xcode8配置总结的相关资料,本文通过图文并茂的形式给大家介绍,非常不错具有参考借鉴价值,需要的朋友可以参考下

  8. IOS11新特性与兼容适配

    iOS 11正式发布了,下面整理了一些该版本下的特点还有如何进行兼容适配工作需要做的事情,希望能够给你提供到帮助。

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

    这篇文章主要介绍了AndroidQ(10)黑暗模式适配的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  10. Android 版本、权限适配相关总结

    针对 Android 6.0 (API 23)已以上版本,Google 增强全新的权限,应用程序在使用敏感权限(如拍照、查阅联系人或存储)时需要先征求用户必须赢得用户同意。

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部