Media Data之多媒体扫描过程分析(二)

此分析代码基于Android 6.0,转载请注明来源地址http://www.jianshu.com/p/71c662bbabd5

Media Data之多媒体扫描过程分析(一)
Media Data之多媒体扫描过程分析(三)

2.1.5 android_media_MediaScanner.cpp

对于android_media_MediaScanner.cpp来说,主要分析三个函数native_init,native_setup和processDirectory。

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }
    //将之后创建的native对象的指针保存到MediaScanner.java的mNativeContext字段中
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

android_media_MediaScanner_native_init的功能主要是动态注册。

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    //获取Stagefright的MediaScanner对象
    MediaScanner *mp = new StagefrightMediaScanner;
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }
    //将对象保存到mNativeContext中
    env->SetLongField(thiz, fields.context, (jlong)mp);
}

android_media_MediaScanner_native_setup方法的作用是创建native的MediaScanner对象,并且用的是StagefrightMediaScanner,等会分析。

static void
android_media_MediaScanner_processDirectory(
        JNIEnv *env, jobject thiz, jstring path, jobject client)
{
    //传入的参数path是需要扫描的路径,client是MediaScannerClient.java对象
    //获取之前保存到mNativeContext的StagefrightMediaScanner对象
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }
    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }
    //构造native层的MyMediaScannerClient对象,参数是java层的MyMediaScannerClient     
    //对象
    MyMediaScannerClient myClient(env, client);
    //调用native层processDirectory方法,参数是扫描路径和native的MyMediaScannerClient 
    //对象
    MediaScanResult result = mp->processDirectory(pathStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning directory '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
}

android_media_MediaScanner_processDirectory方法的作用是启动native层processDirectory扫描方法,在配置过程稍显复杂,其一是java的MediaScanner的上下文环境传递给native额MediaScanner对象中,其二是native的MyMediaScannerClient对象与java的MyMediaScannerClient对象建立联系,方便将结果回调到java层。

2.1.6 MediaScanner.cpp

下面分析的是native层的相关处理,StagefrightMediaScanner.cpp继承自MediaScanner.cpp,在JNI调用的方法processDirectory也是由父类实现的。
先分析MediaScanner.cpp父类的方法。

MediaScanResult MediaScanner::processDirectory(
        const char *path, MediaScannerClient &client) {
    //前期的一些准备工作
    int pathLength = strlen(path);
    if (pathLength >= PATH_MAX) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    char* pathBuffer = (char *)malloc(PATH_MAX + 1);
    if (!pathBuffer) {
        return MEDIA_SCAN_RESULT_ERROR;
    }
    int pathRemaining = PATH_MAX - pathLength;
    strcpy(pathBuffer, path);
    if (pathLength > 0 && pathBuffer[pathLength - 1] != '/') {
        pathBuffer[pathLength] = '/';
        pathBuffer[pathLength + 1] = 0;
        --pathRemaining;
    }
    //设置native的MyMediaScannerClient对象的local信息
    client.setLocale(locale());
    //执行doProcessDirectory方法
    MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
    //释放资源
    free(pathBuffer);
    return result;
}

MediaScanResult MediaScanner::doProcessDirectory(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
    // place to copy file or directory name
    char* fileSpot = path + strlen(path);
    struct dirent* entry;
    if (shouldSkipDirectory(path)) {
        ALOGD("Skipping: %s", path);
        return MEDIA_SCAN_RESULT_OK;
    }
    // Treat all files as non-media in directories that contain a  ".nomedia" file
    if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
        strcpy(fileSpot, ".nomedia");
        if (access(path, F_OK) == 0) {
            ALOGV("found .nomedia, setting noMedia flag");
            noMedia = true;
        }
        // restore path
        fileSpot[0] = 0;
    }
    //打开对应的文件夹路径
    DIR* dir = opendir(path);
    if (!dir) {
        ALOGW("Error opening directory '%s', skipping: %s.", path, strerror(errno));
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    MediaScanResult result = MEDIA_SCAN_RESULT_OK;
    //循环遍历所有文件
    while ((entry = readdir(dir))) {
        //调用doProcessDirectoryEntry方法
        if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
                == MEDIA_SCAN_RESULT_ERROR) {
            result = MEDIA_SCAN_RESULT_ERROR;
            break;
        }
    }
    //关闭文件夹
    closedir(dir);
    return result;
}

MediaScanResult MediaScanner::doProcessDirectoryEntry(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
        struct dirent* entry, char* fileSpot) {
    struct stat statbuf;
    //枚举目录中的文件和子文件夹信息
    const char* name = entry->d_name;
    // ignore "." and ".."
    if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0))) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    int nameLength = strlen(name);
    if (nameLength + 1 > pathRemaining) {
        // path too long!
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    strcpy(fileSpot, name);

    int type = entry->d_type;
    if (type == DT_UNKNOWN) {
        // If the type is unknown, stat() the file instead.
        // This is sometimes necessary when accessing NFS mounted filesystems, but
        // could be needed in other cases well.
        //执行stat方法,获取文件的所有属性,成功返回0失败返回-1
        if (stat(path, &statbuf) == 0) {
            if (S_ISREG(statbuf.st_mode)) {
                type = DT_REG;
            } else if (S_ISDIR(statbuf.st_mode)) {
                type = DT_DIR;
            }
        } else {
            ALOGD("stat() failed for %s: %s", path, strerror(errno) );
        }
    }
    if (type == DT_DIR) {
        bool childNoMedia = noMedia;
        // set noMedia flag on directories with a name that starts with '.'
        // for example, the Mac ".Trashes" directory
        if (name[0] == '.')
            childNoMedia = true;
        // report the directory to the client
        if (stat(path, &statbuf) == 0) {
            //调用MyMediaScannerClient的scanFile函数
            status_t status = client.scanFile(path, statbuf.st_mtime, 0,
                    true /*isDirectory*/, childNoMedia);
            if (status) {
                //返回值是checkAndClearExceptionFromCallback,如果是true就出错
                return MEDIA_SCAN_RESULT_ERROR;
            }
        }
        // and now process its contents
        strcat(fileSpot, "/");
        MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
                client, childNoMedia);
        if (result == MEDIA_SCAN_RESULT_ERROR) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    } else if (type == DT_REG) {
        stat(path, &statbuf);
        status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
                false /*isDirectory*/, noMedia);
        if (status) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    }
    return MEDIA_SCAN_RESULT_OK;
}

从上面的分析中看到调用到了MyMediaScannerClient的scanFile函数,下面分析这个函数

virtual status_t scanFile(const char* path, long long lastModified,
        long long fileSize, bool isDirectory, bool noMedia)
{
    jstring pathStr;
    if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
   //此处的mClient是java层的MyMediaScannerClient,调用的也是java层的scanFile方法
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
            fileSize, isDirectory, noMedia);
    mEnv->DeleteLocalRef(pathStr);
    return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}

可以看出在native层的MyMediaScannerClient调用的是java层MyMediaScannerClient的scanFile函数,下面分析java层的逻辑。

public void scanFile(String path, long lastModified, long fileSize,
        boolean isDirectory, boolean noMedia) {
    // This is the callback funtion from native codes.
    //调用了doScanFile方法
    doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}
public Uri doScanFile(String path, String mimeType, long lastModified,
                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
            //参数scanAlways控制是否强制扫描
            Uri result = null;
            try {
                // beginFile方法的作用主要是1. 生成FileEntry,2.判断是否有修改文件
                FileEntry entry = beginFile(path, mimeType, lastModified,
                        fileSize, isDirectory, noMedia);
                // if this file was just inserted via mtp, set the rowid to zero
                // (even though it already exists in the database), to trigger
                // the correct code path for updating its entry
                if (mMtpObjectHandle != 0) {
                    entry.mRowId = 0;
                }
                // rescan for metadata if file was modified since last scan
                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
                    if (noMedia) {
                        //不是media的情况
                        result = endFile(entry, false, false, false, false, false);
                    } else {
                        //重新扫描获取的信息
                        String lowpath = path.toLowerCase(Locale.ROOT);
                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
                            (!ringtones && !notifications && !alarms && !podcasts);
                        boolean isaudio = MediaFile.isAudioFileType(mFileType);
                        boolean isvideo = MediaFile.isVideoFileType(mFileType);
                        boolean isimage = MediaFile.isImageFileType(mFileType); 
                        if (isaudio || isvideo || isimage) {
                        //如过类型是音频、视频和图片的话,对路径进行处理
                        //If the given path exists on emulated external storage, 
                        //return the translated backing path hosted on internal storage.
                            path = Environment.maybeTranslateEmulatedPathToInternal
                                        (new File(path)).getAbsolutePath();
                        }
                        // we only extract metadata for audio and video files
                        if (isaudio || isvideo) {
                        //调用processFile方法,把MyMediaScannerClient作为参数传入
                        // processFile方法是native方法,稍后分析
                            processFile(path, mimeType, this);
                        }
                        if (isimage) {
                            //如果是图片,单独处理,调用processImageFile方法
                            //Decode a file path into a bitmap.
                            processImageFile(path);
                        }
                    // endFile方法是更新数据库
                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                    }
                }
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
            }
            return result;
        }

从上面的分析可以看到,其实又调用到了processFile方法中,他也是一个native方法,需要再回到jni层继续分析此方法。

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    // Lock already hold by processDirectory
    //获取的还是native层的MediaScanner对象,实际类型是StagefrightMediaScanner对象
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }
    //构造了新的native层的MyMediaScannerClient对象,传入的还是java层的MyMediaScannerClient对象
    MyMediaScannerClient myClient(env, client);
    //调用的是StagefrightMediaScanner对象的processFile方法,等会分析
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

从上面的分析可以看出,调用了StagefrightMediaScanner对象的processFile方法,下面分析此方法。

MediaScanResult StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
    //调用native层的MyMediaScannerClient对象进行local信息,语言设置
    client.setLocale(locale());
    //beginFile方法是由MyMediaScannerClient的父类实现的,其实谷歌并没有实现此方法 
    client.beginFile();
    //具体的方法是调用processFileInternal实现的
    MediaScanResult result = processFileInternal(path, mimeType, client);
    //根据设置的区域信息来对字符串进行转换
    client.endFile();
    return result;
}

MediaScanResult StagefrightMediaScanner::processFileInternal(
        const char *path, const char * /* mimeType */,
        MediaScannerClient &client) {
    //获取扩展名信息
    const char *extension = strrchr(path, '.');
    if (!extension) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    //对扩展名不符合的跳过扫描
    if (!FileHasAcceptableExtension(extension)
        && !AVUtils::get()->isEnhancedExtension(extension)) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    // MediaMetadataRetriever将一个输入媒体文件中设置帧和元数据
    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
    //打开资源
    int fd = open(path, O_RDONLY | O_LARGEFILE);
    status_t status;
    if (fd < 0) {
        // couldn't open it locally, maybe the media server can?
        //打开资源失败
        status = mRetriever->setDataSource(NULL /* httpService */, 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;
        }
    }
    //构造元数据的tag
    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;
        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
            //设置tag和value到MyMediaScannerClient中,稍后分析
            status = client.addStringTag(kKeyMap[i].tag, value);
            if (status != OK) {
                return MEDIA_SCAN_RESULT_ERROR;
            }
        }
    }
    return MEDIA_SCAN_RESULT_OK;
}

从上面的分析中,设置tag和value是通过MyMediaScannerClient调用的,在MyMediaScannerClient的父类MediaScannerClient有addStringTag方法,在方法中又调用了子类MyMediaScannerClient的handleStringTag方法。

status_t MediaScannerClient::addStringTag(const char* name, const char* value)
{
    //调用子类的handleStringTag方法
    handleStringTag(name, value);
    return OK;
}
virtual status_t handleStringTag(const char* name, const char* value)
{
    jstring nameStr, valueStr;
    //获取字符串的值
    if ((nameStr = mEnv->NewStringUTF(name)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
    char *cleaned = NULL;
    //如果value的值不是utf-8编码,则需要特殊处理
    if (!isValidUtf8(value)) {
        cleaned = strdup(value);
        char *chp = cleaned;
        char ch;
        while ((ch = *chp)) {
            if (ch & 0x80) {
                *chp = '?';
            }
            chp++;
        }
        value = cleaned;
    }
    //将处理完成的值赋值到新的字符串valueStr中
    valueStr = mEnv->NewStringUTF(value);
    //释放资源
    free(cleaned);
    if (valueStr == NULL) {
        mEnv->DeleteLocalRef(nameStr);
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
    //调用java层MyMediaScanner的handleStringTag方法
    mEnv->CallVoidMethod(
        mClient, mHandleStringTagMethodID, nameStr, valueStr);
    mEnv->DeleteLocalRef(nameStr);
    mEnv->DeleteLocalRef(valueStr);
    return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
}

此时在native层中又去调用java层的方法了,此处调用的是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.
        //将tag信息中的value值都赋值到了成员变量中
        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();
    ... ...
}

到此文件的读取过程分析完成了,这些成员变量装填完成之后就会调用到endFile方法中,进行更新数据库了。

2.2 IPC方式

由于发广播的方式无法实时地获取连接的状态,所以Android又提供了一种查询方法,就是通过IPC,也就是进程间通信的方式去启动扫描,然后获取扫描的状态。

2.2.1流程图

这里写图片描述

2.2.2MediaScannerConnection.java

/**
 * MediaScannerConnection provides a way for applications to pass a
 * newly created or downloaded media file to the media scanner service.
 * The media scanner service will read metadata from the file and add
 * the file to the media content provider.
 * The MediaScannerConnectionClient provides an interface for the
 * media scanner service to return the Uri for a newly scanned file
 * to the client of the MediaScannerConnection class.
 */

通过注释可以看出,MediaScannerConnection可以提供另一种非发广播的方式去主动扫描文件,他的调用过程是跨进程的,扫描的结果会通过回调函数获得。
在MediaScannerConnection内部提供了两种方式去供客户端使用,一种是实现接口和回调方法,另一种是使用代理模式所提供的静态方法。

(1)实现接口
首先通过构造方法新建实例,并且设置相关的成员变量。然后在客户端处调用connect方法,去绑定service,并且调用requestScanFile方法去跨进程调用MediaScannerService中的方法。当连接到MediaScannerService后回调客户端onMediaScannerConnected方法,当MediaScannerService扫描完成后,回调客户端onScanCompleted方法,整个过程完成。

//监听扫描完成的接口
public interface OnScanCompletedListener {
    public void onScanCompleted(String path, Uri uri);
}
//客户端需要实现的接口,同时也是在服务端所获取的客户端的实例
public interface MediaScannerConnectionClient extends OnScanCompletedListener {
    public void onMediaScannerConnected();
    public void onScanCompleted(String path, Uri uri);
}
//构造方法,传入的参数是客户端的上下文环境和客户端的实例
public MediaScannerConnection(Context context, MediaScannerConnectionClient client) {
    mContext = context;
    mClient = client;
}
// ServiceConnection的回调方法,当service连接时回调
public void onServiceConnected(ComponentName className, IBinder service) {
    synchronized (this) {
        //获取IMediaScannerService的实例mService
        mService = IMediaScannerService.Stub.asInterface(service);
        if (mService != null && mClient != null) {
            //当service连接上时,回调到客户端的onMediaScannerConnected方法
            mClient.onMediaScannerConnected();
        }
    }
}
// IMediaScannerListener是AIDL文件,只有一个方法scanCompleted
//这里获取了服务端IMediaScannerListener的实例
private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() {
    public void scanCompleted(String path, Uri uri) {
        MediaScannerConnectionClient client = mClient;
        if (client != null) {
            //当回调到scanCompleted时,调用客户端的onScanCompleted方法
            client.onScanCompleted(path, uri);
        }
    }
};
//此方法是在客户端处调用,传入需要扫描的路径和文件类型
public void scanFile(String path, String mimeType) {
    synchronized (this) {
        if (mService == null || !mConnected) {
            throw new IllegalStateException("not connected to MediaScannerService");
        }
        try {
            //调用IMediaScannerService的方法
            mService.requestScanFile(path, mimeType, mListener);
        } catch (RemoteException e) {
        }
    }
}
//在客户端调用方法,bindService到MediaScannerService
public void connect() {
    synchronized (this) {
        if (!mConnected) {
            Intent intent = new Intent(IMediaScannerService.class.getName());
            intent.setComponent(
                    new ComponentName("com.android.providers.media",
                            "com.android.providers.media.MediaScannerService"));
            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
            mConnected = true;
        }
    }
}

MediaScannerConnection部分分析完成,可以看出在connect方法中去绑定了远程的MediaScannerService,接下来分析在MediaScannerService完成的操作。

@Override
public IBinder onBind(Intent intent){
    return mBinder;
}

//在绑定之后获取到了服务端的实例,实现requestScanFile的具体方法
private final IMediaScannerService.Stub mBinder = 
        new IMediaScannerService.Stub() {
    //此处是requestScanFile实现的具体方法
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener){
        Bundle args = new Bundle();
        //将相关的参数都放入到了bundle中
        args.putString("filepath", path);
        args.putString("mimetype", mimeType);
        if (listener != null) {
            args.putIBinder("listener", listener.asBinder());
        }
        // 用startService的启动方式去启动,传入bundle
        startService(new Intent(MediaScannerService.this,
                MediaScannerService.class).putExtras(args));
    }
    //此处是scanFile实现的具体方法
    public void scanFile(String path, String mimeType) {
        requestScanFile(path, mimeType, null);
    }
};

//在onStartCommand方法中将intent的值发送到了ServiceHandler处理
private final class ServiceHandler extends Handler {
    @Override
    public void handleMessage(Message msg)
    {
        Bundle arguments = (Bundle) msg.obj;
        String filePath = arguments.getString("filepath");
        
        try {
            if (filePath != null) {
                //从intent中获取IBinder对象
                IBinder binder = arguments.getIBinder("listener");
                //获取IMediaScannerListener的实例
                IMediaScannerListener listener = (binder == null ? null :
                       IMediaScannerListener.Stub.asInterface(binder));
                Uri uri = null;
                try {
                    uri = scanFile(filePath, arguments.getString("mimetype"));
                } catch (Exception e) {
                    Log.e(TAG, "Exception scanning file", e);
                }
                if (listener != null) {
                    //查询完成后,回调到IMediaScannerListener,客户端处也随之回调
                    listener.scanCompleted(filePath, uri);
                }
            ... ...

在MediaScannerService的主要作用就是接受intent,调用scanFile方法扫描,扫描完成之后调用回调方法,给客户端回调。

(2) 静态方法实现
在MediaScannerConnection也可以通过提供的静态方法去实现扫描。其原理就是实现代理模式,远程代理客户端的实例进行相关操作,客户端只需要传入相应的参数即可,不需要手动连接service等操作,比较方便实用。

public static void scanFile(Context context, String[] paths, String[] mimeTypes,
        OnScanCompletedListener callback) {
    //实例化ClientProxy,并给构造函数传参
    ClientProxy client = new ClientProxy(paths, mimeTypes, callback);
    //实例化MediaScannerConnection,并给构造函数传参
    MediaScannerConnection connection = new MediaScannerConnection(context, client);
    client.mConnection = connection;
    //调用connect函数
    connection.connect();
}

//客户端的远程代理类
static class ClientProxy implements MediaScannerConnectionClient {
    final String[] mPaths;
    final String[] mMimeTypes;
    final OnScanCompletedListener mClient;
    MediaScannerConnection mConnection;
    int mNextPath;
    //构造函数,配置参数
    ClientProxy(String[] paths, String[] mimeTypes, OnScanCompletedListener client) {
        mPaths = paths;
        mMimeTypes = mimeTypes;
        mClient = client;
    }
    //实现回调方法
    public void onMediaScannerConnected() {
        scanNextPath();
    }
    public void onScanCompleted(String path, Uri uri) {
        if (mClient != null) {
            mClient.onScanCompleted(path, uri);
        }
        scanNextPath();
    }
    //因为传入的路径是数组,进行循环扫描
    void scanNextPath() {
        if (mNextPath >= mPaths.length) {
            mConnection.disconnect();
            return;
        }
        String mimeType = mMimeTypes != null ? mMimeTypes[mNextPath] : null;
        mConnection.scanFile(mPaths[mNextPath], mimeType);
        mNextPath++;
    }
}

所以对于客户端来说,实现此静态方法去扫描,只需要传入上下文,查询的路径(可以是多个路径,用数组表示),文件类型和监听器即可,不需要考虑其他,比较方便使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容