Android 之MediaScanner流程解析

时间:2014-05-07 16:29:15   收藏:0   阅读:696

MediaScanner详解

OK, 我们现在开始来大概分析一下android framework中MediaScanner部分的流程,若大家发现分析过程中有错误,欢迎拍砖指正。

分析流程之前,我们先给自己定个要用MediaScanner解决的问题,这样我们才会有目标感,才知道我们要干什么。否则,干巴巴的分析流程,一般都会很容易的迷失在各种code的迷雾中。

我们这里要定的目标是:获取某个MP3文件的artist & album

我们可以假定,现在有个媒体播放器,在播放music的时候,需要在UI上显示出来artist& album.

从UI的角度来说,android提供了两套可以获取mediaInfo的方法:

1.      通过MediaScanner  Scan文件后,从DB中查询相关信息;

2.      直接通过android提供的MediaMetadataRetriever类来获取相关信息,基本用法如下:

public void getMetaData(String filePath) {

    String title, album,artist, composer, genre, mime;

    MediaMetadataRetriever retriever =newMediaMetadataRetriever();

    retriever.setDataSource(filePath);

 

    title =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);

    album =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);

    artist =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);

    composer =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER);

    genre =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);

    mime =retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE);

 

Log.i(TAG, “title=” +title + “,album=” + album + “,artist=” + artist + “,composer=” + composer +“,genre=” + genre + “,mime=” + mime);

}

既然我们现在要说的是MediaScanner,那自然我们要介绍的是通过第一种方法获取相关信息的流程了。

 

OverView

我们这里会分为3个层次来详细解读一下MediaScanner的工作流程:

1.      UI APK调用的接口;

2.      Java层调用flow;

3.      Native层调用flow;

bubuko.com,布布扣bubuko.com,布布扣

bubuko.com,布布扣

UI调用接口

现在我们的目标是获取某个MP3文件的artist& album,因此自然需要分为两步:

1.      Scan media file

2.      Query data

 

1.     Scan Media File


public void scanfile(path)

{

String[] paths = new String[1];

//设置需要扫描的文件路径,这里path就是我们所要扫描的music file的路径

paths[0] = path;       

// 调用MediaScannerConnectionscanFile,进行扫描。

//scanFile的第三个参数是mimeTypes,若为null,则会根据文件后缀来判断。

//scanCbMediaScannerConnection.OnScanCompletedListener

    MediaScannerConnection.scanFile(this.context, paths, null, scanCb);

}

2.     Query data

Scan完成之后,就可以在DB中查询相关的audio信息。

private void getInfo()

 {

    String[] colume = { "_id", "album_id", "title", "artist", "album", "year", "duration", "_size", "_data" };

    StringBuilder localStringBuilder = new StringBuilder();

    localStringBuilder.append("_data");

    localStringBuilder.append(" LIKE ‘%");

    localStringBuilder.append(musicUtils.convertToQueryString(this.mPath));

    localStringBuilder.append("%‘");

localStringBuilder.append(" ESCAPE ‘\\‘");

/*colume是要选择的列,localStringBuilder是选择的条件,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI是要查询的URI*/

    Cursor localCursor =query(this.context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, colume, localStringBuilder.toString(), null, null);

    if (localCursor != null);

    try

{

//查询到的结果会存储在localCursor中,随后可从localCursor中获取相关信息

      if (localCursor.moveToFirst())

      {

        mSongId = localCursor.getLong(0);

        mAlbumId = localCursor.getLong(1);

        mTitle = localCursor.getString(2);

        mSonger = localCursor.getString(3);

        mAlbum = localCursor.getString(4);

        mReleaseYear = localCursor.getInt(5);

        mDuration = localCursor.getLong(6);

        mSize = localCursor.getLong(7);

        mDataPath = localCursor.getString(8);

    }

    catch (RuntimeException localRuntimeException)

    {

    }

    finally

    {

      if (localCursor != null)

        localCursor.close();

    }

 }

 

这里查询的URI为MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,对应的db存储的路径为/data/data/com.android.providers.media/external.db。可用SQLiteSpy查看此DB的内容。

至此从UI的角度来看,已经完成了获取mp3文件info的任务。

 

Java层MediaScanflow

我们这里主要分析Scanfile的流程,不再对查询信息的流程进行解析。

OK,先看java层的时序图:

bubuko.com,布布扣bubuko.com,布布扣


MediaScannerClient主要用途是给app提供一个和MediaScannerService交互的桥梁。App可以通过MediaScannerClient给MediaScanenrService传递需要scan的文件。Mediascanner service会对传入的文件进行scan,读取metadata,并将文件添加进mediacontent provider。MediaScannerConnectionClient也为mediascanner service提供了一个返回最近被scan的文件的Uri。

MediaScannerService类其实起到的主要作用就是创建出来MediaScanner,之后调用MediaScanner的ScanFile接口进行工作,最终做具体工作的其实是MediaScanner和MyMediaScannerClient。

 

下面我们从头追一下code。

Scan动作的发起者是MediaScannerConnection.

Code位于\frameworks\base\media\java\android\media\MediaScannerConnection.java:

 

public static void scanFile(Context context, String[] paths, String[] mimeTypes,

            OnScanCompletedListener callback) {

        ClientProxy client = new ClientProxy(paths, mimeTypes, callback);

        MediaScannerConnection connection = new MediaScannerConnection(context, client);

        client.mConnection = connection;

       //scanFile函数只是调用了一下MediaScannerConnection的connect()函数。

        connection.connect();

    }

 

MediaScannerConnection中有两个名叫scanFile的函数,不过一个是public static的,另外一个则是普通的private函数。Static修饰的scanFile是对UI的接口。通过上面的code可以看出,其实scanFile并未做任何scan的动作,只是new了一个MediaScannerConnection,调用了一下它的connect()函数。

我们再来看一下MediaScannerConnection的connect()函数:

public void connect() {

        synchronized (this) {

            if (!mConnected) {

                Intent intent = new Intent(IMediaScannerService.class.getName());

                mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);

                mConnected = true;

            }

        }

    }

当Service绑定之后,会触发MediaScannerConnection的onServiceConnected函数,而在这个函数中,又调用了ClientProxy的onMediaScannerConnected函数(ClientProxy继承自MediaScannerConnectionClient,ClientProxy是在调用MediaScannerConnection的scanFile函数时new 出来的,大家可以回过头看看前面的scanFile函数),在onMediaScannerConnected中,会调用ClientProxy的scanNextPath(),scanNextPath()中会调用MediaScannerConnection的scanFile()【注意,这里的scanFile就是我们之前提到过的privatescanFile】。scanFile()中会调用到IMediaScannerService的requestScanFile。在requestScanFile中则会启动MediaScannerService.这部分代码比较简单,就不一一列举出来了。

到现在为止,看看我们有什么了。从scanFile开始一直到requestScanFile,我们最终启动了MediaScannerService。那么下面的任务就交给MediaScannerService来完成了。

 

MediaScannerService

MediaScannerService.java在packages\providers\MediaProvider\src\com\android\providers\media目录下。

在MediaScannerService启动的时候会new一个Looper,在Looper中又new了一个ServiceHandler。

下来我们看一下MediaScannerService的onStartCommand

public int onStartCommand(Intent intent, int flags, int startId)

    {

        while (mServiceHandler == null) {

            synchronized (this) {

                try {

                    wait(100);

                } catch (InterruptedException e) {

                }

            }

        }

 

        if (intent == null) {

            Log.e(TAG, "Intent is null in onStartCommand: ",

                new NullPointerException());

            return Service.START_NOT_STICKY;

        }

 

        Message msg = mServiceHandler.obtainMessage();

        msg.arg1 = startId;

        msg.obj = intent.getExtras();

        //此处给mServiceHandler发送了一个消息,其handlerMessage会收到这个消息。

        mServiceHandler.sendMessage(msg);

 

        // Try again later if we are killed before we can finish scanning.

        return Service.START_REDELIVER_INTENT;

    }

 

接着看下handlerMessage做了什么事情:

public void handleMessage(Message msg)

        {

            Bundle arguments = (Bundle) msg.obj;

            String filePath = arguments.getString("filepath");

           

            try {

                //我们传入了MP3文件路径,所以会走这个分支

                if (filePath != null) {

                    IBinder binder = arguments.getIBinder("listener");

                    IMediaScannerListener listener =

                            (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));

                    Uri uri = null;

                    try {

                        //这里才开始了scanFile的流程。此时传入的mimetypenull.

                        uri = scanFile(filePath, arguments.getString("mimetype"));

                    } catch (Exception e) {

                        Log.e(TAG, "Exception scanning file", e);

                    }

                    if (listener != null) {

                        //scan 完成之后,通知listener

                        listener.scanCompleted(filePath, uri);

                    }

                } else {

                    ……

                }

            } catch (Exception e) {

                Log.e(TAG, "Exception in handleMessage", e);

            }

 

            stopSelf(msg.arg1);

        }

 

我们来看一下MediaScannerService的scanFile都干了什么。

private Uri scanFile(String path, String mimeType) {

        String volumeName = MediaProvider.EXTERNAL_VOLUME;

        //打开数据库。这个MediaProvider.EXTERNAL_VOLUMEexternal

        openDatabase(volumeName);

        //创建MediaScanner,并设置其language&country

        MediaScanner scanner = createMediaScanner();

        try {

            // make sure the file path is in canonical form

            String canonicalPath = new File(path).getCanonicalPath();

           //扫描文件

            return scanner.scanSingleFile(canonicalPath, volumeName, mimeType);

        } catch (Exception e) {

            Log.e(TAG, "bad path " + path + " in scanFile()", e);

            return null;

        }

    }

 

MediaScanner被创建时,先会设置language & country,之后会调用到jni层的接口native_init()& native_setup(),这里最主要的其实是在native_setup中创建native层的MediaScanner。这里我们创建的其实是一个StagefrightMediaScanner。这个我们后面再说。

MediaScanner的scanSingleFile会调用到MyMediaScannerClient(位置也在MediaScaner.java中)doScanFile。

MyMediaScannerClient

 

几乎所有的事情都是在MyMediaScannerClient中完成的。

public Uri doScanFile(String path, String mimeType, long lastModified,

                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {

            Uri result = null;

            ……

 

                        boolean isaudio = MediaFile.isAudioFileType(mFileType);

                        boolean isvideo = MediaFile.isVideoFileType(mFileType);

                        boolean isimage = MediaFile.isImageFileType(mFileType);

 

                        ……

 

                        // we only extract metadata for audio and video files

                        if (isaudio || isvideo) {

                            /*processFile是一个native方法,最终实现的地方在native层的StagefrightMediaScanner,获取media filemetadata,都是在这个函数中完成的*/

                            processFile(path, mimeType, this);

                        }

 

                        if (isimage) {

                            processImageFile(path);

                        }

                                                                                               

                         /*获取到的Metadata存入db,是由endFile完成的。*/

                        result = endFile(entry, ringtones, notifications, alarms, music, podcasts);

                               

            ……

 

            return result;

        }

 

获取到的metaData会被暂时保存在MyMediaScannerClient的成员变量中,之后通过endFile,将这些成员变量的值通过MediaProvider存入DB中。

这里还要提到一个函数:MyMediaScannerClient的handleStringTag():

public void handleStringTag(String name, String value) {

            if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {

                // Don‘t trim() here, to preserve the special \001 character

                // used to force sorting. The media provider will trim() before

                // inserting the title in to the database.

                mTitle = value;

            } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {

                mArtist = value.trim();

            } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")

                    || name.equalsIgnoreCase("band") || name.startsWith("band;")) {

                mAlbumArtist = value.trim();

            } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) {

                mAlbum = value.trim();

            } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) {

                mComposer = value.trim();

            } else if (mProcessGenres &&

                    (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) {

                mGenre = getGenreName(value);

            } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) {

                mYear = parseSubstring(value, 0, 0);

            } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) {

                // track number might be of the form "2/12"

                // we just read the number before the slash

                int num = parseSubstring(value, 0, 0);

                mTrack = (mTrack / 1000) * 1000 + num;

            } else if (name.equalsIgnoreCase("discnumber") ||

                    name.equals("set") || name.startsWith("set;")) {

                // set number might be of the form "1/3"

                // we just read the number before the slash

                int num = parseSubstring(value, 0, 0);

                mTrack = (num * 1000) + (mTrack % 1000);

            } else if (name.equalsIgnoreCase("duration")) {

                mDuration = parseSubstring(value, 0, 0);

            } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {

                mWriter = value.trim();

            } else if (name.equalsIgnoreCase("compilation")) {

                mCompilation = parseSubstring(value, 0, 0);

            } else if (name.equalsIgnoreCase("isdrm")) {

                mIsDrm = (parseSubstring(value, 0, 0) == 1);

            } else if (name.equalsIgnoreCase("width")) {

                mWidth = parseSubstring(value, 0, 0);

            } else if (name.equalsIgnoreCase("height")) {

                mHeight = parseSubstring(value, 0, 0);

            } else {

                //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")");

            }

        }

此函数最终调用的地方,其实是通过JNI被native层的MediaScannerClient的endfile调用。用来将通过MediaMetadataRetriever获取到的metadata,送给java层MyMediaScannerClient的成员变量中。

至此,java层MediaScan的流程就算全部结束了。

 

Native 层MediaScanner  flow

废话不多说,先上时序图:

bubuko.com,布布扣bubuko.com,布布扣

在上一节我们提到过在MyMediaScannerClient的doScanFile中,会调用到一个native层的函数processFile.

这个processFile实现的地方在StagefrightMediaScanner类中。位置在:frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp中。

MediaScanResult StagefrightMediaScanner::processFile(

        const char *path, const char *mimeType,

        MediaScannerClient &client) {

    ALOGV("processFile ‘%s‘.", path);

 

    //设置mLocaleEncoding,此变量用以在endfile中判断编码格式。

client.setLocale(locale());

//beginFile很简单,就是new了两个buffer,用以存放metadata的名称和值

client.beginFile();

/*processFileInternal中通过MediaMetadataRetriever,来获取到metadata,并将其一一对应的放入在beginFilenew出来的两个buffer(mNames&mValues)之中。*/

MediaScanResult result = processFileInternal(path, mimeType, client);

/*endFile中将得到的metadata,先转码成UTF8,之后通过JNI调用到javahandleStringTag函数,将metadata相关信息保存到MyMediaScannerClient的成员变量中,前面已经有提到过了*/

    client.endFile();

    return result;

}

 

我们来看一下processFileInternal:

MediaScanResult StagefrightMediaScanner::processFileInternal(

        const char *path, const char *mimeType,

        MediaScannerClient &client) {

    ……

 

    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);

 

    int fd = open(path, O_RDONLY | O_LARGEFILE);

    status_t status;

           

//1. 通过MediaMetadataRetrieversetDataSource

    if (fd < 0) {

        // couldn‘t open it locally, maybe the media server can?

        status = mRetriever->setDataSource(path);

    } else {

        status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);

        close(fd);

    }

 

    if (status) {

        return MEDIA_SCAN_RESULT_ERROR;

    }

 

const char *value;

    if ((value = mRetriever->extractMetadata(

                    METADATA_KEY_MIMETYPE)) != NULL) {

        status = client.setMimeType(value);

        if (status) {

            return MEDIA_SCAN_RESULT_ERROR;

        }

    }

 

    struct KeyMap {

        const char *tag;

        int key;

    };

    static const KeyMap kKeyMap[] = {

        { "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },

        { "discnumber", METADATA_KEY_DISC_NUMBER },

        { "album", METADATA_KEY_ALBUM },

        { "artist", METADATA_KEY_ARTIST },

        { "albumartist", METADATA_KEY_ALBUMARTIST },

        { "composer", METADATA_KEY_COMPOSER },

        { "genre", METADATA_KEY_GENRE },

        { "title", METADATA_KEY_TITLE },

        { "year", METADATA_KEY_YEAR },

        { "duration", METADATA_KEY_DURATION },

        { "writer", METADATA_KEY_WRITER },

        { "compilation", METADATA_KEY_COMPILATION },

        { "isdrm", METADATA_KEY_IS_DRM },

        { "width", METADATA_KEY_VIDEO_WIDTH },

        { "height", METADATA_KEY_VIDEO_HEIGHT },

    };

    static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);

 

    for (size_t i = 0; i < kNumEntries; ++i) {

        const char *value;

       //2. 通过extractMetadata获取相关key的值

        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {

            // 3. 存值。将获取到的值添加进在beginnew出来的buffer

            status = client.addStringTag(kKeyMap[i].tag, value);

            if (status != OK) {

                return MEDIA_SCAN_RESULT_ERROR;

            }

        }

    }

 

    return MEDIA_SCAN_RESULT_OK;

}

 

至此,metadata就已经保存在了native层MediaScannerClient的mNames和mValues中。

接下来我们看一下MediaScannerClient的endFile。

此endFile的主要作用有两个:

1.      将mValues中的值转成UTF8格式;

2.      将转码后的值,通过JNI保存到java层的MyMediaScannerClient中

 

void MediaScannerClient::endFile()

{

        if (mLocaleEncoding != kEncodingNone) {

        int size = mNames->size();

        uint32_t encoding = kEncodingAll;

        //获取mValues中各个值可能的编码格式

        for (int i = 0; i < mNames->size(); i++) {

            encoding &= possibleEncodings(mValues->getEntry(i));

                                }

 

        // if the locale encoding matches, then assume we have a native encoding.

        //转码成UTF8

        if (encoding & mLocaleEncoding)

            convertValues(mLocaleEncoding);

 

        // finally, push all name/value pairs to the client

        for (int i = 0; i < mNames->size(); i++) {

              //将转码后的value值存放至java层的MyMediaScannerClient中。

              /* handleStringTag函数是通过JNI调用到java层的。有兴趣追一下此函数的话,可以看一下android_media_MediaScanner.cpp文件。此函数最终实现的地方在MediaScaner.javaMyMediaScannerClient类中,前面已经将此函数code列出来了,此处不再做过多解释*/

                status_t status = handleStringTag(mNames->getEntry(i), mValues->getEntry(i));

 

            if (status) {

                break;

            }

        }

    }

    // else addStringTag() has done all the work so we have nothing to do

 

    delete mNames;

    delete mValues;

    mNames = NULL;

    mValues = NULL;

}

 

 

OK,到这里,MediaScanner的所有流程就已经走完了。

我们再过来回过头整理一下scanfile的几个重要地方:

1.      Native_setup(): 在此函数中要创建出来真正干活的MediaScanner出来。

2.      setLocal():要设置正确的language

3.      processFile:Scan file

4.      native层的endFile:要将scan到的value值进行正确的转码,否则存入db中的数据就可能是乱码了。

5.      java层的endFile:此处是真正的将scan到的metadata存入了db中。

 

 

Game Over!!!



Android 之MediaScanner流程解析,布布扣,bubuko.com

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!