第2节 获取视频信息
要知道设备上有哪些可以被播放的视频文件,一般来讲有两个方法,
- 遍历设备磁盘上所有的目录,根据文件的后缀名,把这些目录中所有的视频文件都找出来;
- 向安卓系统提供的
Media Provider
提出查询请求,从而获取我们希望的视频文件信息;
从“减小开发难度,利用安卓系统自身功能,选择最简单的方案”的角度出发,我们采用Media Provider
。
2.1 ContentProvider
ContentProvider
是安卓系统的四大组件之一,为别的组件(Activity、Service)“提供内容”。它就像是一个拥有某种数据的网站,安卓系统运行的其它组件可以通过“网址”访问这个网站,获取需要查询的数据。
ContentProvider
可以是私有的,只能为它所在的应用提供数据访问请求;ContentProvider
也可以是公开的,为别的应用程序提供数据访问请求。当我们自己开发一个
ContentProvider
的时候,就要在应用的AndroidManifest.xml
文件中声明它的存在,
<application
android:allowBackup="true"
....../>
<activity android:name=".MyActivity" />
<provider
android:name=".MyContentProvider"
android:authorities="com.anddle.videoplayer"
android:exported="false"/> -->false为私有的,true为公开的
</application>
从这里也可以看出,它和Activity的地位是一样的,所以与Activity一样并称为安卓系统的四大组件之一。
2.2 系统级的ContentProvider
安卓系统上,有一个叫做Media Provider
的ContentProvider
。它作为系统级别的应用程序在系统上运行,专门负责收集多媒体文件(音频、视频、文件)相关的信息。
Media Provider
在开机启动后,会在后台“监听”磁盘上文件的变化,特定情况下,会自动更新多媒体文件的信息,例如磁盘上是否增加了媒体文件,是否被删除了媒体文件,有的媒体文件名称是否发生了修改等等。
所以当任何应用想获取这类文件相关的信息时,就可以向Media Provider
发起查询的请求。Media Provider
帮我们完成了视频文件信息的收集,因此,我们就不用自己去遍历磁盘上的文件进行视频文件的收集和整理了。
除了Media Provider
,系统还提供了
Contacts Provider
:用来查询联系人信息;Calendar Provider
:用来提供日历相关信息的查询;Bookmark Provider
:用来提供书签信息的查询;
…
其它的就不再一一列出了。
2.3 使用Media Provider
的缺点
使用Media Provider
有一点需要注意,Media Provider
对磁盘多媒体文件的“监控”,并不是实时的。当删除磁盘上一个已有的视频文件时,Media Provider
并不会马上知道,而是要等到下一次的扫描之后,才会更新这个信息。因此,如果使用Media Provider
做为我们提供视频信息的来源,就要考虑到“一个视频刚好被修改了,但是还没有来得及在Media Provider
中更新信息”的情况。
2.4 Media Provider
查询视频文件
- 确定向
Media Provider
发出查询请求的地址-uri,它就像访问网站时,要输入的网址一样。系统提供了两个位置的uri,一个是指向内部存储的uri,一个是指向外部存储的uri。我们要查询的视频文件都是存放在外部存储地址上的;Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
- 确定要请求的视频文件信息。在视频列表中,我们需要展示视频的标题、创建时间,还需要播放它时使用的文件所在地址。这些信息在
Media Provider
中都对应着查询它们使用的字段名称;String[] searchKey = new String[] { MediaStore.Video.Media.TITLE, -->对应文件的标题 MediaStore.Images.Media.DATA, -->对应文件的存放位置 MediaStore.Images.Media.DATE_ADDED -->对应文件的创建时间 };
- 确定查询的条件。我们之前假设过只关心那些叫做
Video
的目录。因此我们要确定的只是查询到的文件路径中,包含有/Video
这个字段。String where = MediaStore.Video.Media.DATA + " like \"%"+"/Video"+"%\"";
这个条件参数的写法就和
SQL
数据库语言的语法一样。这里我们不打算讲SQL
语法,只要知道在我们这个例子中这样使用就好了; - 设定查询结果的排序方式,使用默认的排序方式就可以了,
String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;
- 获取ContentResolver对象,让它使用前面的参数向
Media Provider
发起查询请求;查询的结果存放在Cursor
--指标当中;ContentResolver resolver = getContentResolver(); Cursor cursor = resolver.query( uri, searchKey, where, null, sortOrder);
- 遍历
Cursor
,得到它指向的每一条查询到的信息;当Cursor
指向某条数据的时候,我们就获取它携带的每个字段的值;while(cursor.moveToNext()) { String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)); String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE)); String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)); ...... }
- 存放获取的视频文件信息,创建一个
VideoItem类
,public class VideoItem { String name; String path; Bitmap thumb; String createdTime; VideoItem(String strPath, String strName, String createdTime) { this.path = strPath; this.name = strName; ...... } }
将视频文件创建的时间从Unix格式的时间戳,转换成“年月日分”这种可读的形势,
VideoItem(String strPath, String strName, String createdTime) { ...... SimpleDateFormat sf = new SimpleDateFormat("yy年MM月dd日HH时mm分"); Date d = new Date(Long.valueOf(createdTime)*1000); this.createdTime = sf.format(d); }
获取视频文件的缩略图,Android SDK提供了一个利用视频文件地址获取视频缩略图的工具,用起来非常简单的,通过它将得到缩略图的Bitmap;
VideoItem(String strPath, String strName, String createdTime) { ...... this.thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND); }
图片生成的尺寸可以通过第二个参数设置,
MINI_KIND
表示小的缩略图;
FULL_SCREEN_KIND
表示大尺寸的缩略图;MICRO_KIND
表示超小图的缩略图。这里我们采用的是MINI_KIND
; Cursor
使用完了之后要把它关闭掉,“`java
cursor.close();
“`
整理一下前面的各个步骤,获取外部存储上Video目录
中所有视频文件的方式如下,
Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
String[] searchKey = new String[] {
MediaStore.Video.Media.TITLE,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_ADDED
};
String [] keywords = null;
String where = MediaStore.Video.Media.DATA + " like \"%"+"/Video"+"%\"";
String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(
uri,
searchKey,
where,
keywords,
sortOrder);
if(cursor != null)
{
while(cursor.moveToNext())
{
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));
String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));
VideoItem item = new VideoItem(path, name, createdTime);
......
}
cursor.close();
}
最后一点千万不要忘记,要在应用的AndroidManifest.xml
文件中,添加读取外部存储器的权限,
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.anddle.anddleplayer">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
......
</manifest>
第3节 异步方式获取视频信息
获取视频信息所需要的时间是个不能确定的事情。如果视频很少,也许几十毫秒就能完成,如果视频很多(比如几十个),也许就要花二十多秒。
安卓应用只有一个主线程-各个组件都是在这个线程中运行。作为组件的之一的Activity就是在这个线程中更新应用界面的,例如,用户点击界面上的一个按钮,按钮得到响应,整个过程就是在这个主线程里。所以这个主线程绝对不可以做耗时的操作。假如在按钮中做了耗时的操作,那么当它进行耗时操作的时候,你去点击界面上的其它按钮是不会有反应的,就好像程序冻在了那里。
我们的代码一旦连续占用这个线程超过一定的时间,系统就会弹出“程序无响应的”提示,这个提示叫做ANR
-Applicatin No Response。
因此,我们可以考虑把获取视频信息的操作放到一个单独的线程thread中进行。
这就好比你在正在做一件事情A,突然另一件事情B来打扰你,你不得不停下手头的工作来完成,做完了才能继续之前的工作;这时如果有另外一个人(另一个线程)来帮助你,把事情B全部包揽了,那你就不用分心了。当另一个人把事情B做完后,告诉你一声就可以了。
3.1 异步操作
启动一个新的线程,分担耗时工作的方法是一种异步操作:我让你帮我做一件事情,布置任务后,我就去做其他的事情了,等你做完了再告诉我结果;
与它对应的是同步操作:我让你帮我做一件事情,布置任务后,我啥也不做,就等着你做完了告诉我结果;
获取视频信息是个异步操作,启动一个新线程-工作线程thread-查询视频信息,查询完成后,工作线程再将结果通知到主线程,让主线程将查询到结果的结果显示到界面上。界面的更新一定要在主线程中进行,不能在别的线程修改,否则系统后提示运行错误,这一点相当重要。因此我们一定要将查询的结果发送给主线程,让主线程处理界面的更新。
3.2 异步操作的方案
安卓系统提供的异步操作方案有:
- 创建工作线程thread和Handler,利用Handler在工作线程和主线程之间传递数据;
- 使用AsyncTask帮助类,AsyncTask中封装了工作线程,通过AsyncTask完成工作线程和主线程之间的数据传递;
这里虽然将AsyncTask看成是一个单独的方案,但实际上它也是通过方案1
实现的,只不过对于使用者来讲更加方便而已。
这里我们选择方案2
。因为,
- 使用场景简单,只是单个任务的异步操作,没有多个线程之间的数据同步考虑;
- 使用方便,不用考虑太多的新线程创建的细节;
3.3 AsyncTask的使用
3.3.1 AsyncTask的用法
AsyncTask需要被继承成为一个新的子类来使用,在被继承时,要指定三种参数的类型-Param
Progress
Result
,还需要实现doInBackground(Param...)
函数,此外通常还要实现onProgressUpdate(Progress...)
onPostExecute(Result)
两个回调函数。
class MyTask extends AsyncTask<Param, Progress, Result> {
@Override
protected Result doInBackground(Param... params) {
return result;
}
@Override
protected void onProgressUpdate(Progress... progresses) {
}
@Override
protected void onPostExecute(Result result) {
}
@Override
protected void onCancelled() {
}
}
doInBackground(Param... params)
函数:传入参数的Param
类型就是AsyncTask<Param, Progress, Result>
中指定的Param
类型。它运行在新创建的工作线程当中。使用
MyTask
时,要在主线程中使用excute()
方法传入不定长参数,让Task
运行起来,MyTask task = new MyTask(); task.excute(param0, param1, ..., paramN);
不定长参数会以数组的形式传递到
doInBackground()
函数当中,@Override protected Result doInBackground(Param... params) { Param param0 = params[0]; Param param1 = params[1]; ...... Param paramN = params[N]; return result; }
onProgressUpdate(Progress... progresses)
函数:传入参数的Progress
类型就是AsyncTask<Param, Progress, Result>
中指定的Progress
类型。在
doInBackground()
中执行的是一个很耗时的工作,有时需要向主线程报告当前的运行状况,这就要使用到publishProgress()
函数,publishProgress()
也是使用的不定长参数,@Override protected Result doInBackground(Param... params) { ...... publishProgress(progress1, progress2, ..., progressN) return result; }
不定长参数会以数组的形式传递到
onProgressUpdate()
函数当中,@Override protected void onProgressUpdate(Progress... progresses) { Progress progress0 = progresses[0]; Progress progress1 = progresses[1]; ...... Progress progressN = progresses[N]; }
onPostExecute(Result result)
函数:传入参数的Result
类型就是AsyncTask<Param, Progress, Result>
中指定的Result
类型。doInBackground()
函数返回的类型也是Result
@Override protected Result doInBackground(Param... params) { ...... return result; }
返回的结果作为参数传递给
onPostExecute()
函数,@Override protected void onPostExecute(Result result) { }
onCancel()
函数会在调用者取消AsyncTask
的工作的时候被触发。要取消
AsyncTask
的工作,首先要在主线程中调用cancel()
方法,task.cancel(true);
因为在
doInBackground()
中执行的是一个很耗时的工作,需要时不时的检查自己是否被取消执行了,@Override protected Result doInBackground(Param... params) { ...... if(isCancelled()) { ...... return result; } ...... return result; }
最后,
onCancelled()
函数会被触发,这个函数会在主线程中被执行,@Override protected void onCancelled() { }
综合上面的分析,自定义一个
AsyncTask
的方法如下,
class MyTask extends AsyncTask<Param, Progress, Result> {
@Override
protected Result doInBackground(Param... params) {
Param param0 = params[0];
Param param1 = params[1];
......
Param paramN = params[N];
while(!isCancelled())
{
......
publishProgress(progress1, progress2, ..., progressN);
}
return result;
}
@Override
protected void onProgressUpdate(Progress... progresses) {
Progress progress0 = progresses[0];
Progress progress1 = progresses[1];
......
Progress progressN = progresses[N];
......
}
@Override
protected void onPostExecute(Result result) {
}
@Override
protected void onCancelled() {
}
}
使用一个AsyncTask
的方法如下,
MyTask task = new MyTask();
task.excute(param0, param1, ..., paramN);
......
task.cancel(true);
3.3.2 获取视频信息的AsyncTask
根据我们的需要,自己定义个AsyncTask
-VideoUpdateTask
,
- 不需要为新创建的线程传入参数;所以
Param
设置成Object
; - 因为查询的过程很长,所以需要时不时通知主线程查询的状态,每查询到一条,就将视频数据传递给主线程;所以
Progress
设置成VideoItem
; - 查询的结果已经在查询的过程中发送给了主线程,全部完成后,不需要再传递什么结果给主线程了,所以
Result
设置成Void
; - 将查询视频信息的操作放到
doInBackground()
中进行,这是一个新创建的工作线程; - 工作线程中,每发现一个视频,就通知给主线程;
class VideoUpdateTask extends AsyncTask<Object, VideoItem, Void> {
@Override
protected Void doInBackground(Object... params) {
Uri uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
String[] searchKey = new String[] {
MediaStore.Video.Media.TITLE,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_ADDED
};
String [] keywords = null;
String where = MediaStore.Video.Media.DATA + " like \"%"+"/Video"+"%\"";
String sortOrder = MediaStore.Video.Media.DEFAULT_SORT_ORDER;
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(
uri,
searchKey,
where,
keywords,
sortOrder);
if(cursor != null)
{
while(cursor.moveToNext() && ! isCancelled())
{
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Video.Media.TITLE));
String createdTime = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED));
VideoItem item = new VideoItem(path, name, createdTime);
publishProgress(item);
}
cursor.close();
}
return null;
}
@Override
protected void onProgressUpdate(VideoItem... progresses) {
VideoItem item = (VideoItem) progresses[0];
//更新界面
......
}
@Override
protected void onPostExecute(Result result) {
//更新界面
......
}
@Override
protected void onCancelled() {
//更新界面
......
}
}
在视频列表Activity创建的时候,启动VideoUpdateTask
,开始查询符合我们要求的视频信息。
private AsyncTask mVideoUpdateTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_list);
mVideoUpdateTask = new VideoUpdateTask();
mVideoUpdateTask.execute();
}
在视频列表Activity退出的时候,判断VideoUpdateTask
是否还在运行,如果还在运行,就让它停止,
@Override
protected void onDestroy() {
super.onDestroy();
if((mVideoUpdateTask != null) &&
(mVideoUpdateTask.getStatus() == AsyncTask.Status.RUNNING))
{
mVideoUpdateTask.cancel(true);
}
mVideoUpdateTask = null;
}
onCreate()
与onDestroy()
是Activity生命周期的一部分,当一个Activity被创建的时候会调用到onCreate()
,当Activity被退出销毁的时候会调用到onDestroy()
。所以在这两个地方使用VideoUpdateTask
是一个合适的选择。