Android MediaProvider

本文以 Android 9.0 为准

Android 系统提供了对多媒体的统一处理机制,通过一套良好的框架实现了多媒体信息的扫描、存储、读取。用户可以基于这套框架非常方便的对多媒体信息进行处理,这套框架主要包含了三部分:

  • MediaScannerReceiver:多媒体扫描广播接收者,继承 BroadcastReceiver,主要响应APP发送的广播命令,并开启 MediaScannerService 执行扫描工作。
  • MediaScannerService:多媒体扫描服务,继承 Service,主要是处理 APP 发送的请求,要用到 Framework 中的 MediaScanner 来共同完成具体扫描工作,并获取媒体文件的 metadata,最后将数据写入或删除 MediaProvider 提供的数据库中。
  • MediaProvider:多媒体内容提供者,继承 ContentProvider,主要是负责操作数据库,并提供给别的程序 insert、query、delete、update 等操作。

本文就从上面三部分作为入口,分析它们是如何工作的,如何对设备上的多媒体进行扫描,如何将多媒体信息进行存储,用户如何读取、修改多媒体信息?


1. 如何调用 MediaScannerService?

1.1 MediaScannerActivity

我们可以从 Android 自带的 Dev Tools 中的 MediaScannerActivity 入手,看看它是如何扫描多媒体。
/development/apps/Development/src/com/android/development/MediaScannerActivity.java

package com.android.development;

public class MediaScannerActivity extends Activity
{
    private TextView mTitle;

    /** Called when the activity is first created or resumed. */
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        setContentView(R.layout.media_scanner_activity);

        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_STARTED);
        intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
        intentFilter.addDataScheme("file");
        registerReceiver(mReceiver, intentFilter);

        mTitle = (TextView) findViewById(R.id.title);
    }

    /** Called when the activity going into the background or being destroyed. */
    @Override
    public void onDestroy() {
        unregisterReceiver(mReceiver);
        super.onDestroy();
    }

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
                mTitle.setText("Media Scanner started scanning " + intent.getData().getPath());
            }
            else if (intent.getAction().equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
                mTitle.setText("Media Scanner finished scanning " + intent.getData().getPath());
            }
        }
    };

    public void startScan(View v) {
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"
                + Environment.getExternalStorageDirectory())));

        mTitle.setText("Sent ACTION_MEDIA_MOUNTED to trigger the Media Scanner.");
    }
}

主要做了两件事:

  • 注册扫描开始和结束的广播,用来展示扫描状态;
  • 在点击事件中,发送了 ACTION_MEDIA_MOUNTED 广播。

那么系统肯定存在一个的接收者,在收到 ACTION_MEDIA_MOUNTED 后进行扫描,这就是 MediaScannerReceiver。

1.2 MediaScannerReceiver

首先关注 AndroidManifest,对接受的广播一目了然。
/packages/providers/MediaProvider/AndroidManifest.xml


    
        
        
    
    
        
        
    
    
        
        
    
    
        
        
    

/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerReceiver.java

package com.android.providers.media;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;

import java.io.File;
import java.io.IOException;

public class MediaScannerReceiver extends BroadcastReceiver {
    private final static String TAG = "MediaScannerReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final Uri uri = intent.getData();
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            // 开机广播,只处理内部存储
            scan(context, MediaProvider.INTERNAL_VOLUME);
        } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
            // 处理系统语言变换
            scanTranslatable(context);
        } else {
            if (uri.getScheme().equals("file")) {
                // 处理外部存储
                String path = uri.getPath();
                String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
                String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();

                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    Log.e(TAG, "couldn't canonicalize " + path);
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                }

                Log.d(TAG, "action: " + action + " path: " + path);
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                    // 每当挂载外部存储时
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    // 扫描单个文件,并且路径是在外部存储路径下
                    scanFile(context, path);
                }
            }
        }
    }

    // 扫描内部或者外部存储,根据volume进行区分
    private void scan(Context context, String volume) {
        Bundle args = new Bundle();
        args.putString("volume", volume);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }

    // 扫描单个文件,不可以是文件夹
    private void scanFile(Context context, String path) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }

    // 扫描可转换语言的多媒体
    private void scanTranslatable(Context context) {
        final Bundle args = new Bundle();
        args.putBoolean(MediaStore.RETRANSLATE_CALL, true);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }
}

再对比下 Android 6.0 中 MediaScannerReceiver 源码:

package com.android.providers.media;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.IOException;

public class MediaScannerReceiver extends BroadcastReceiver {
    private final static String TAG = "MediaScannerReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final Uri uri = intent.getData();
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            // Scan both internal and external storage
            scan(context, MediaProvider.INTERNAL_VOLUME);
            scan(context, MediaProvider.EXTERNAL_VOLUME);

        } else {
            if (uri.getScheme().equals("file")) {
                // handle intents related to external storage
                String path = uri.getPath();
                String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
                String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();

                try {
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    Log.e(TAG, "couldn't canonicalize " + path);
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                }

                Log.d(TAG, "action: " + action + " path: " + path);
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                    // scan whenever any volume is mounted
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    scanFile(context, path);
                }
            }
        }
    }

    private void scan(Context context, String volume) {
        Bundle args = new Bundle();
        args.putString("volume", volume);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }

    private void scanFile(Context context, String path) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
    }
}

扫描的时机为以下几点:

  1. Intent.ACTION_BOOT_COMPLETED.equals(action)
    6.0 中接到设备重启的广播,对 Internal 和 External 扫描,而 9.0 中只对 Internal 扫描。
  2. Intent.ACTION_LOCALE_CHANGED.equals(action)
    9.0 相比 6.0 增加了系统语言发生改变时的广播,用于进行扫描可以转换语言的多媒体。
  3. uri.getScheme().equals("file")
    6.0 和 9.0 处理的一致,都是先过滤 scheme 为 "file" 的 Intent,再通过下面两个 action 对 External 进行扫描:
    • Intent.ACTION_MEDIA_MOUNTED.equals(action)
      插入外部存储时扫描 scan()。
    • Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) && path != null && path.startsWith(externalStoragePath + "/")
      扫描外部存储中的单个文件 scanFile()。

注意:不支持扫描外部存储中的文件夹,需要遍历文件夹中文件,使用扫描单个文件的方式。


2. MediaScannerService 如何工作?

MediaScannerService 继承 Service,并实现 Runnable 工作线程。通过 ServiceHandler 这个 Handler 把主线程需要大量计算的工作放到工作线程中。
/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java

2.1 在 onCreate() 中启动工作线程:

@Override
public void onCreate() {
    // 获取电源锁,防止扫描过程中休眠
    PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    // 获取外部存储扫描路径
    StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
    mExternalStoragePaths = storageManager.getVolumePaths();

    // Start up the thread running the service.  Note that we create a
    // separate thread because the service normally runs in the process's
    // main thread, which we don't want to block.
    Thread thr = new Thread(null, this, "MediaScannerService"); // 启动最重要的工作线程,该线程也是个消息泵线程
    thr.start();
}

可以看到,onCreate() 里会启动最重要的工作线程,该线程也是个消息泵线程。每当用户需要扫描媒体文件时,基本上都是在向这个消息泵里发送 Message,并在处理 Message 时完成真正的 scan 动作。请注意,创建 Thread 时传入的第二个参数就是 MediaScannerService 自身,也就是说线程的主要行为其实就是 MediaScannerService 的 run() 方法,该方法的代码如下:

@Override
public void run() {
    // reduce priority below other background threads to avoid interfering
    // with other services at boot time.
    // 设置进程优先级,媒体扫描比较费时,防止 CPU 一直被 MediaScannerService 占用
    // 这会导致用户感觉系统变得很慢
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
            Process.THREAD_PRIORITY_LESS_FAVORABLE);
    Looper.prepare();

    mServiceLooper = Looper.myLooper(); // 消息looper
    mServiceHandler = new ServiceHandler(); // 发送消息的handler

    Looper.loop();
}

后续就是通过上面那个 mServiceHandler 向消息队列发送 Message 的。

2.2 向工作线程发送 Message

比较常见的向消息泵发送 Message 的做法是调用 startService(),并在 MediaScannerService 的 onStartCommand() 方法里 sendMessage()。比如,和 MediaScannerService 配套提供的 MediaScannerReceiver,当它收到类似 ACTION_BOOT_COMPLETED 这样的系统广播时,就会调用自己的 scan() 或 scanFile() 方法,里面的 startService() 动作会导致走到 service 的 onStartCommand(),并进一步发送消息,其方法截选如下:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    ...
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent.getExtras();
    mServiceHandler.sendMessage(msg); // 发送消息

    // Try again later if we are killed before we can finish scanning.
    return Service.START_REDELIVER_INTENT;
}

另外一种比较常见的发送 Message 的做法是先直接或间接 bindService(),绑定成功后会得到一个 IMediaScannerService 接口,而后外界再通过该接口向 MediaScannerService 发起命令,请求其扫描特定文件或目录。

IMediaScannerService 接口只提供了两个接口方法:

  • void requestScanFile(String path, String mimeType, in IMediaScannerListener listener);
  • void scanFile(String path, String mimeType);

处理这两种请求的实体是服务内部的 mBinder 对象,参考代码如下:

private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() {
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener) {
        if (false) {
            Log.d(TAG, "IMediaScannerService.scanFile: " + path + " mimeType: " + mimeType);
        }
        Bundle args = new Bundle();
        args.putString("filepath", path);
        args.putString("mimetype", mimeType);
        if (listener != null) {
            args.putIBinder("listener", listener.asBinder());
        }
        startService(new Intent(MediaScannerService.this,
                MediaScannerService.class).putExtras(args));
    }

    public void scanFile(String path, String mimeType) {
        requestScanFile(path, mimeType, null);
    }
};

说到底还是在调用 startService()。

具体处理消息泵线程里的消息时,执行的是 ServiceHandler 的 handleMessage() 方法:

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) {
                // 扫描单个文件
                ...
                try {
                    uri = scanFile(filePath, arguments.getString("mimetype"));
                } catch (Exception e) {
                    Log.e(TAG, "Exception scanning file", e);
                }
                ...
            } else if (arguments.getBoolean(MediaStore.RETRANSLATE_CALL)) {
                // 切换语言
                ContentProviderClient mediaProvider = getBaseContext().getContentResolver()
                    .acquireContentProviderClient(MediaStore.AUTHORITY);
                mediaProvider.call(MediaStore.RETRANSLATE_CALL, null, null);
            } else {
                // 扫描内部或外部
                String volume = arguments.getString("volume");
                String[] directories = null;

                if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
                    // 如果是扫描内部存储,实际扫描目录为
                    directories = new String[] {
                            Environment.getRootDirectory() + "/media",
                            Environment.getOemDirectory() + "/media",
                            Environment.getProductDirectory() + "/media",
                }
                else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                    // 如果是扫描外部存储,实际扫描目录为
                    if (getSystemService(UserManager.class).isDemoUser()) {
                        directories = ArrayUtils.appendElement(String.class,
                                mExternalStoragePaths,
                                Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
                    } else {
                        directories = mExternalStoragePaths;
                    }
                }
                // 调用 scan 函数开展文件夹扫描工作
                if (directories != null) {
                    scan(directories, volume);
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Exception in handleMessage", e);
        }

        stopSelf(msg.arg1); // 扫描结束,MediaScannerService完成本次使命,可以stop自身了
    }
}

MediaScannerService中 的 scanFile() 方法,用来扫描单个文件:

private Uri scanFile(String path, String mimeType) {
    String volumeName = MediaProvider.EXTERNAL_VOLUME;

    try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
        // make sure the file path is in canonical form
        String canonicalPath = new File(path).getCanonicalPath();
        return scanner.scanSingleFile(canonicalPath, mimeType);
    } catch (Exception e) {
        Log.e(TAG, "bad path " + path + " in scanFile()", e);
        return null;
    }
}

MediaScannerService 中的 scan() 方法,用来扫描内部或外部存储的路径:

private void scan(String[] directories, String volumeName) {
    Uri uri = Uri.parse("file://" + directories[0]);
    // don't sleep while scanning
    mWakeLock.acquire();

    try {
        ContentValues values = new ContentValues();
        values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
        // 通过 insert 这个特殊的 uri,让 MeidaProvider 做一些准备工作
        Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
        // 发送开始扫描的广播
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

        try {
            if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                // 打开数据库文件
                openDatabase(volumeName);
            }
            // 创建媒体扫描器,并调用 scanDirectories 扫描目标文件夹
            try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                scanner.scanDirectories(directories);
            }
        } catch (Exception e) {
            Log.e(TAG, "exception in MediaScanner.scan()", e);
        }
        // 通过 delete 这个 uri,让 MeidaProvider 做一些清理工作
        getContentResolver().delete(scanUri, null, null);

    } finally {
        // 发送结束扫描的广播
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
        mWakeLock.release();
    }
}

上面的代码中,比较复杂的是 MediaScannerService 和 MediaProvider 的交互。MediaScannerService 经常使用一些特殊 Uri 做数据库操作,而 MediaProvider 针对这些 Uri 会走一些特殊的处理,例如打开数据库文件等,后面在 MediaProvider 中会重点说到,我们还先回到扫描的逻辑。

scanFile() 或 scan() 才是实际进行扫描的地方,扫描动作中主要借助的是 MediaScanner,它是打通 Java 层和 C++ 层的关键,扫描动作最终会调用到 MediaScanner的某个 native 函数,于是程序流程开始走到 C++ 层。

现在,我们可以画一张示意图:

Android MediaProvider_第1张图片

3. MediaScanner 如何工作?

顾名思义,MediaScanner 就是个“媒体文件扫描器”。它必须打通 Java 层和 C++ 层。请大家注意它的两个 native 函数:native_init() 和 native_setup(),以及两个重要成员变量:一个是mClient成员,另一个是 mNativeContext,后面会详细说明。
/frameworks/base/media/java/android/media/MediaScanner.java
MediaScanner的相关代码截选如下:

public class MediaScanner implements AutoCloseable {
    static {
        System.loadLibrary("media_jni");
        native_init();    // 将java层和c++层联系起来
    }
    ...
    private long mNativeContext;
    ...
    public MediaScanner(Context c, String volumeName) {
        native_setup();
        ...
    }
    ...
    // 一开始就具有明确的mClient对象
    private final MyMediaScannerClient mClient = new MyMediaScannerClient();
    ...
}

MediaScanner 类加载之时,就会同时加载动态链接库“media_jni”,并调用 native_init() 将 Java 层和 C++ 层联系起来。
/frameworks/base/media/jni/android_media_MediaScanner.cpp

// This function gets a field ID, which in turn causes class initialization.
// It is called from a static block in MediaScanner, which won't run until the
// first time an instance of this class is used.
static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    // Java 层 MediaScanner 类
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }
    // Java 层 mNativeContext 对象(long 类型)保存在 JNI 层 fields.context 对象中
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

经过分析代码,我们发现在 C++ 层会有个与 MediaScanner 相对应的类,叫作 StagefrightMediaScanner。当 Java层创建 MediaScanner 对象时,MediaScanner 的构造函数就调用了 native_setup(),该函数对应到 C++ 层就是 android_media_MediaScanner_native_setup(),其代码如下:
/frameworks/base/media/jni/android_media_MediaScanner.cpp

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    ALOGV("native_setup");
    // 创建 native 层的 MediaScanner 对象,StagefrightMediaScanner(frameworks/av/ 中定义)
    MediaScanner *mp = new StagefrightMediaScanner;

    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }
    // 将 mp 指针保存在 Java 层 MediaScanner 类 mNativeContext 对象中
    env->SetLongField(thiz, fields.context, (jlong)mp);
}

最后一句 env->SetLongField() 其实就是在为 Java 层 MediaScanner 的 mNativeContext 域赋值。

后续我们会看到,每当 C++ 层执行扫描动作时,还会再创建一个 MyMediaScannerClient 对象,这个对象和 Java 层的同名类对应。我们画一张图来说明:

Android MediaProvider_第2张图片
image

3.1 scanSingleFile() 动作

// this function is used to scan a single file
public Uri scanSingleFile(String path, String mimeType) {
    try {
        prescan(path, true); // ① 扫描前预准备

        File file = new File(path);
        if (!file.exists() || !file.canRead()) {
            return null;
        }

        // lastModified is in milliseconds on Files.
        long lastModifiedSeconds = file.lastModified() / 1000;

        // always scan the file, so we can return the content://media Uri for existing files
        // ② 扫描前预准备
        return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(),
                false, true, MediaScanner.isNoMediaPath(path));
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
        return null;
    } finally {
        releaseResources();
    }
}

先看①处代码, prescan 函数比较关键,首先让我们试想一个问题。

在媒体扫描过程中,有个令人头疼的问题,来举个例子:假设某次扫描前 SD 卡中有 100 个媒体文件,数据库中会有 100 条关于这些文件的记录。现删除其中的 50 个文件,那么媒体数据库什么时候会被更新呢?

MediaScanner 考虑到了这一点,prescan 函数的主要作用就是在扫描之前把上次扫描获取的数据库信息取出遍历并检测是否丢失,如果丢失,则从数据库中删除。

再看②处代码,借助了 mClient.doScanFile(),此处的 mClient 类型为 MyMediaScannerClient。

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    try {
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
        ...

        // rescan for metadata if file was modified since last scan
        if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
            if (noMedia) {
                result = endFile(entry, false, false, false, false, false);
            } else {
                ...
                // we only extract metadata for audio and video files
                if (isaudio || isvideo) {
                    mScanSuccess = processFile(path, mimeType, this);
                }

                if (isimage) {
                    mScanSuccess = processImageFile(path);
                }
                ...
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    }
    ...
    return result;
}

因为 MyMediaScannerClient 是 MediaScanner 的内部类,所以它可以直接调用 MediaScanner 的 processFile()。

现在我们画一张 MediaScannerService.scanFile() 的调用关系图:

Android MediaProvider_第3张图片
scanFile() 的调用关系图

3.2 scanDirectories() 动作

public void scanDirectories(String[] directories) {
    try {
        prescan(null, true);  // 扫描前预准备
        ...
        for (int i = 0; i < directories.length; i++) {
            // native 函数,调用它来对目标文件夹进行扫描
            processDirectory(directories[i], mClient); 
        }
        ...
        postscan(directories);  // 扫描后处理
    } catch (SQLException e) {
        ...
    } finally {
        ...
    }
}

我们画一张 MediaScannerService .scan() 的调用关系图:

Android MediaProvider_第4张图片
scan() 的调用关系图

4. 调用到 C++ 层

这里就不深入展开,可以看下这篇文章《MediaScannerService研究》。


5. MediaProvider 如何工作

/packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java

5.1 MediaProvider 何时创建数据库

通常来说,数据库的创建,应该在 ContentProvider 的 onCreate() 方法中完成 。

@Override
public boolean onCreate() {
    ...
    // DatabaseHelper缓存
    mDatabases = new HashMap();
    // 绑定内部存储数据库
    attachVolume(INTERNAL_VOLUME);
    ...
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
         // 如果已挂载外部存储,绑定外部存储数据库
        attachVolume(EXTERNAL_VOLUME);
    }
    ...
    return true;
}

接下来分析attachVolume方法:

创建数据库:

  • 如果此存储卷已经链接上了,则不执行任何操作。
  • 否则,查询存储卷的 ID 并且建立对应的数据库。
/**
 * Attach the database for a volume (internal or external).
 * Does nothing if the volume is already attached, otherwise
 * checks the volume ID and sets up the corresponding database.
 *
 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
 * @return the content URI of the attached volume.
 */
private Uri attachVolume(String volume) {
    ...
    // Update paths to reflect currently mounted volumes
    // 更新路径以反映当前装载的卷
    updateStoragePaths();

    DatabaseHelper helper = null;
    synchronized (mDatabases) {
        helper = mDatabases.get(volume);
        // 判断是否已经attached过了
        if (helper != null) {
            if (EXTERNAL_VOLUME.equals(volume)) {
                // 确保默认的文件夹已经被创建在挂载的主要存储设备上,
                // 对每个存储卷只做一次这种操作,所以当用户手动删除时不会打扰
                ensureDefaultFolders(helper, helper.getWritableDatabase());
            }
            return Uri.parse("content://media/" + volume);
        }

        Context context = getContext();
        if (INTERNAL_VOLUME.equals(volume)) {
            // 如果是内部存储则直接实例化DatabaseHelper
            helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
                    false, mObjectRemovedCallback);
        } else if (EXTERNAL_VOLUME.equals(volume)) {
            // 如果是外部存储的操作,只获取主要的外部卷 ID
            // Only extract FAT volume ID for primary public
            final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume();
            if (vol != null) {// 判断是否存在主要的外部卷
                // 获取主要的外部卷
                final StorageVolume actualVolume = mStorageManager.getPrimaryVolume();
                // 获取主要的外部卷 ID
                final int volumeId = actualVolume.getFatVolumeId();

                // Must check for failure!
                // If the volume is not (yet) mounted, this will create a new
                // external-ffffffff.db database instead of the one we expect.  Then, if
                // android.process.media is later killed and respawned, the real external
                // database will be attached, containing stale records, or worse, be empty.
                // 数据库都是以类似 external-ffffffff.db 的形式命名的,
                // 后面的 8 个 16 进制字符是该 SD 卡 FAT 分区的 Volume ID。
                // 该 ID 是分区时决定的,只有重新分区或者手动改变才会更改,
                // 可以防止插入不同 SD 卡时数据库冲突。
                if (volumeId == -1) {
                    String state = Environment.getExternalStorageState();
                    if (Environment.MEDIA_MOUNTED.equals(state) ||
                            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
                        // This may happen if external storage was _just_ mounted.  It may also
                        // happen if the volume ID is _actually_ 0xffffffff, in which case it
                        // must be changed since FileUtils::getFatVolumeId doesn't allow for
                        // that.  It may also indicate that FileUtils::getFatVolumeId is broken
                        // (missing ioctl), which is also impossible to disambiguate.
                        // 已经挂载但是sd卡是只读状态
                        Log.e(TAG, "Can't obtain external volume ID even though it's mounted.");
                    } else {
                        // 还没有挂载
                        Log.i(TAG, "External volume is not (yet) mounted, cannot attach.");
                    }

                    throw new IllegalArgumentException("Can't obtain external volume ID for " +
                            volume + " volume.");
                }

                // generate database name based on volume ID
                // 根据volume ID设置数据库的名称
                String dbName = "external-" + Integer.toHexString(volumeId) + ".db";
                // 创建外部存储数据库
                helper = new DatabaseHelper(context, dbName, false,
                        false, mObjectRemovedCallback);
                mVolumeId = volumeId;
            } else {
                // external database name should be EXTERNAL_DATABASE_NAME
                // however earlier releases used the external-XXXXXXXX.db naming
                // for devices without removable storage, and in that case we need to convert
                // to this new convention
                // 外部数据库名称应为EXTERNAL_DATABASE_NAME
                // 但是较早的版本对没有可移动存储的设备使用external-XXXXXXXX.db命名
                // 在这种情况下,我们需要转换为新的约定
                ...
                // 根据之前转换的数据库名,创建数据库
                helper = new DatabaseHelper(context, dbFile.getName(), false,
                        false, mObjectRemovedCallback);
            }
        } else {
            throw new IllegalArgumentException("There is no volume named " + volume);
        }
        // 缓存起来,标识已经创建过了数据库
        mDatabases.put(volume, helper);
        ...
    }

    if (EXTERNAL_VOLUME.equals(volume)) {
        // 给外部存储创建默认的文件夹
        ensureDefaultFolders(helper, helper.getWritableDatabase());
    }
    return Uri.parse("content://media/" + volume);
}

先来关注一下 getPrimaryPhysicalVolume() 这个相关的方法:
/frameworks/base/core/java/android/os/storage/StorageManager.java

// 获取主要的外部的 VolumeInfo
public @Nullable VolumeInfo getPrimaryPhysicalVolume() {
    final List vols = getVolumes();
    for (VolumeInfo vol : vols) {
        if (vol.isPrimaryPhysical()) {
            return vol;
        }
    }
    return null;
}

/frameworks/base/core/java/android/os/storage/VolumeInfo.java

// 判断该 VolumeInfo 是否是主要的,并且是外部的
public boolean isPrimaryPhysical() {
    return isPrimary() && (getType() == TYPE_PUBLIC);
}

// 判断该 VolumeInfo 是否是主要的
public boolean isPrimary() {
    return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
}

下面就是分析创建数据库的源头DatabaseHelper:

/**
 * Creates database the first time we try to open it.
 */
@Override
public void onCreate(final SQLiteDatabase db) {
    // 在此方法中对700版本以下的都会新建数据库
    updateDatabase(mContext, db, mInternal, 0, getDatabaseVersion(mContext));
}

/**
 * Updates the database format when a new content provider is used
 * with an older database format.
 */
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
    // 对数据库进行更新
    mUpgradeAttempted = true;
    updateDatabase(mContext, db, mInternal, oldV, newV);
}

这里强调一句 getDatabaseVersion() 方法获取的 fromVersion 不是数据库版本,而是 /packages/providers/MediaProvider/AndroidManifest.xml 中的 versionCode。

现在已经找到创建数据库的方法updateDatabase,现在大致分析一下此方法:

/**
 * This method takes care of updating all the tables in the database to the
 * current version, creating them if necessary.
 * This method can only update databases at schema 700 or higher, which was
 * used by the KitKat release. Older database will be cleared and recreated.
 * @param db Database
 * @param internal True if this is the internal media database
 */
private static void updateDatabase(Context context, SQLiteDatabase db, boolean internal,
        int fromVersion, int toVersion) {
    ...
    // 对不同版本的数据库进行判断
    if (fromVersion < 700) {
        // 小于700,重新创建数据库
        createLatestSchema(db, internal);
    } else if (fromVersion < 800) {
        // 对700-800之间的数据库处理
        updateFromKKSchema(db);
    } else if (fromVersion < 900) {
        // 对800-900之间的数据库处理
        updateFromOCSchema(db);
    }
    // 检查audio_meta的_data值是否是不同的,如果不同就删除audio_meta,
    // 在扫描的时候重新创建
    sanityCheck(db, fromVersion);
}

那么还有一个疑惑,我们知道 ContentProvider 的 onCreate() 执行时间,早于 Application onCreate(),那么在 onCreate() 之后挂载外部存储,是如何处理的呢?

搜索 attachVolume() 的调用位置,可以找到在 insertInternal() 中看到:

private Uri insertInternal(Uri uri, int match, ContentValues initialValues,
                           ArrayList notifyRowIds) {
    ...
    switch (match) {
        ...
        case VOLUMES:
        {
            String name = initialValues.getAsString("name");
            // 根据name绑定存储数据库
            Uri attachedVolume = attachVolume(name);
            ...
            return attachedVolume;
        }
    ...
}

根据 VOLUMES 找到对应的 URI:

URI_MATCHER.addURI("media", null, VOLUMES);

而调用 insertInternal() 方法的地方,是在 insert() 方法中。

那么说明,必然存在一个调用 insert() 方法,并传入了 "content://media/" 的URI,可以在 MediaScannerService 的 openDatabase() 方法中找到:
/packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java

private void openDatabase(String volumeName) {
    try {
        ContentValues values = new ContentValues();
        values.put("name", volumeName);
        getContentResolver().insert(Uri.parse("content://media/"), values);
    } catch (IllegalArgumentException ex) {
        Log.w(TAG, "failed to open media database");
    }         
}

调用 openDatabase() 方法的地方就是在开始扫描外部存储的时候,也就在这个时候,进行了 DatabaseHelper 的实例化,在前文已经分析了 scan() 的代码,为了方便查看,这里再列该方法:

private void scan(String[] directories, String volumeName) {
    Uri uri = Uri.parse("file://" + directories[0]);
    // don't sleep while scanning
    mWakeLock.acquire();

    try {
        ContentValues values = new ContentValues();
        values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
        Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

        try {
            if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                openDatabase(volumeName);
            }

            try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                scanner.scanDirectories(directories);
            }
        } catch (Exception e) {
            Log.e(TAG, "exception in MediaScanner.scan()", e);
        }

        getContentResolver().delete(scanUri, null, null);

    } finally {
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
        mWakeLock.release();
    }
}

至此,对于数据库的创建已经分析完毕。

5.2 MediaProvider 更新

@Override
public int (Uri uri, ContentValues initialValues, String userWhere,
        String[] whereArgs) {
    // 将uri进行转换成合适的格式,去除标准化
    uri = safeUncanonicalize(uri);
    int count;
    // 对uri进行匹配
    int match = URI_MATCHER.match(uri);
    // 返回查询的对应uri的数据库帮助类
    DatabaseHelper helper = getDatabaseForUri(uri);
    // 记录更新的次数
    helper.mNumUpdates++;
    // 通过可写的方式获得数据库实例
    SQLiteDatabase db = helper.getWritableDatabase();
    String genre = null;
    if (initialValues != null) {
        // 获取流派的信息,然后删除掉
        genre = initialValues.getAsString(Audio.AudioColumns.GENRE);
        initialValues.remove(Audio.AudioColumns.GENRE);
    }
    ...
    // 根据匹配的uri进行相应的操作
    switch (match) {
        case AUDIO_MEDIA:
        case AUDIO_MEDIA_ID:
        // 更新音乐人和专辑字段。首先从缓存中判断是否有值,如果有直接用缓存中的
        // 数据,如果没有再从数据库中查询是否有对应的信息,如果有则更新,
        // 如果没有插入这条数据.接下来的操作是增加更新次数,并更新流派
        ...
        case IMAGES_MEDIA:
        case IMAGES_MEDIA_ID:
        case VIDEO_MEDIA:
        case VIDEO_MEDIA_ID:
        // 更新视频,并且发出生成略缩图请求
        ...
        case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
        // 更新播放列表数据
        ...
    }
    ...
}

至此,更新操作已完成。

5.3 MediaProvider 插入

关于插入,有两个方法插入,一个是大量的插入 bulkInsert 方法传入的是 ContentValues 数组;一个是 insert,传入的是单一个 ContentValues。下面分别分析:

@Override
public int bulkInsert(Uri uri, ContentValues values[]) {
    // 首先对传入的Uri进行匹配
    int match = URI_MATCHER.match(uri);
    if (match == VOLUMES) {
        // 如果是匹配的是存储卷,则直接调用父类的方法,进行循环插入
        return super.bulkInsert(uri, values);
    }
    // 对DatabaseHelper和SQLiteDatabase的初始化
    DatabaseHelper helper = getDatabaseForUri(uri);
    if (helper == null) {
        throw new UnsupportedOperationException(
                "Unknown URI: " + uri);
    }
    SQLiteDatabase db = helper.getWritableDatabase();
    if (db == null) {
        throw new IllegalStateException("Couldn't open database for " + uri);
    }

    if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) {
        // 插入播放列表的数据,在playlistBulkInsert中是开启的事务进行插入
        return playlistBulkInsert(db, uri, values);
    } else if (match == MTP_OBJECT_REFERENCES) {
        // 将MTP对象的ID转换成音频的ID,最终也是调用到playlistBulkInsert
        int handle = Integer.parseInt(uri.getPathSegments().get(2));
        return setObjectReferences(helper, db, handle, values);
    }

    ArrayList notifyRowIds = new ArrayList();
    int numInserted = 0;
    // insert may need to call getParent(), which in turn may need to update the database,
    // so synchronize on mDirectoryCache to avoid deadlocks
    synchronized (mDirectoryCache) {
         // 如果不满足上述的条件,则开启事务进行插入其他的数据
        db.beginTransaction();
        try {
            int len = values.length;
            for (int i = 0; i < len; i++) {
                if (values[i] != null) {
                    // 循环调用insertInternal去插入相关的数据
                    insertInternal(uri, match, values[i], notifyRowIds);
                }
            }
            numInserted = len;
            db.setTransactionSuccessful();
        } finally {
            // 结束事务
            db.endTransaction();
        }
    }

    // 通知更新
    getContext().getContentResolver().notifyChange(uri, null);
    return numInserted;
}

@Override
public Uri insert(Uri uri, ContentValues initialValues) {
    int match = URI_MATCHER.match(uri);

    ArrayList notifyRowIds = new ArrayList();
    // 只是调用insertInternal进行插入
    Uri newUri = insertInternal(uri, match, initialValues, notifyRowIds);

    // do not signal notification for MTP objects.
    // we will signal instead after file transfer is successful.
    if (newUri != null && match != MTP_OBJECTS) {
        // Report a general change to the media provider.
        // We only report this to observers that are not looking at
        // this specific URI and its descendants, because they will
        // still see the following more-specific URI and thus get
        // redundant info (and not be able to know if there was just
        // the specific URI change or also some general change in the
        // parent URI).
        getContext().getContentResolver().notifyChange(uri, null, match != MEDIA_SCANNER
                ? ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS : 0);
        // Also report the specific URIs that changed.
        if (match != MEDIA_SCANNER) {
            getContext().getContentResolver().notifyChange(newUri, null, 0);
        }
    }
    return newUri;
}

5.4 MediaProvider 删除

@Override
public int delete(Uri uri, String userWhere, String[] whereArgs) {
    uri = safeUncanonicalize(uri);
    int count;
    int match = URI_MATCHER.match(uri);

    // handle MEDIA_SCANNER before calling getDatabaseForUri()
    if (match == MEDIA_SCANNER) {
        if (mMediaScannerVolume == null) {
            return 0;
        }
        DatabaseHelper database = getDatabaseForUri(
                Uri.parse("content://media/" + mMediaScannerVolume + "/audio"));
        if (database == null) {
            Log.w(TAG, "no database for scanned volume " + mMediaScannerVolume);
        } else {
            database.mScanStopTime = SystemClock.currentTimeMicro();
            String msg = dump(database, false);
            logToDb(database.getWritableDatabase(), msg);
        }
        if (INTERNAL_VOLUME.equals(mMediaScannerVolume)) {
            // persist current build fingerprint as fingerprint for system (internal) sound scan
            final SharedPreferences scanSettings =
                    getContext().getSharedPreferences(MediaScanner.SCANNED_BUILD_PREFS_NAME,
                            Context.MODE_PRIVATE);
            final SharedPreferences.Editor editor = scanSettings.edit();
            editor.putString(MediaScanner.LAST_INTERNAL_SCAN_FINGERPRINT, Build.FINGERPRINT);
            editor.apply();
        }
        mMediaScannerVolume = null;
        pruneThumbnails();
        return 1;
    }

    if (match == VOLUMES_ID) {
        detachVolume(uri);
        count = 1;
    } else if (match == MTP_CONNECTED) {
        synchronized (mMtpServiceConnection) {
            if (mMtpService != null) {
                // MTP has disconnected, so release our connection to MtpService
                getContext().unbindService(mMtpServiceConnection);
                count = 1;
                // mMtpServiceConnection.onServiceDisconnected might not get called,
                // so set mMtpService = null here
                mMtpService = null;
            } else {
                count = 0;
            }
        }
    } else {
        final String volumeName = getVolumeName(uri);
        final boolean isExternal = "external".equals(volumeName);

        DatabaseHelper database = getDatabaseForUri(uri);
        if (database == null) {
            throw new UnsupportedOperationException(
                    "Unknown URI: " + uri + " match: " + match);
        }
        database.mNumDeletes++;
        SQLiteDatabase db = database.getWritableDatabase();

        TableAndWhere tableAndWhere = getTableAndWhere(uri, match, userWhere);
        if (tableAndWhere.table.equals("files")) {
            String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
            if (deleteparam == null || ! deleteparam.equals("false")) {
                database.mNumQueries++;
                Cursor c = db.query(tableAndWhere.table,
                        sMediaTypeDataId,
                        tableAndWhere.where, whereArgs,
                        null /* groupBy */, null /* having */, null /* orderBy */);
                String [] idvalue = new String[] { "" };
                String [] playlistvalues = new String[] { "", "" };
                MiniThumbFile imageMicroThumbs = null;
                MiniThumbFile videoMicroThumbs = null;
                try {
                    while (c.moveToNext()) {
                        final int mediaType = c.getInt(0);
                        final String data = c.getString(1);
                        final long id = c.getLong(2);

                        if (mediaType == FileColumns.MEDIA_TYPE_IMAGE) {
                            deleteIfAllowed(uri, data);
                            MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                    volumeName, FileColumns.MEDIA_TYPE_IMAGE, id);

                            idvalue[0] = String.valueOf(id);
                            database.mNumQueries++;
                            Cursor cc = db.query("thumbnails", sDataOnlyColumn,
                                        "image_id=?", idvalue,
                                        null /* groupBy */, null /* having */,
                                        null /* orderBy */);
                            try {
                                while (cc.moveToNext()) {
                                    deleteIfAllowed(uri, cc.getString(0));
                                }
                                database.mNumDeletes++;
                                db.delete("thumbnails", "image_id=?", idvalue);
                            } finally {
                                IoUtils.closeQuietly(cc);
                            }
                            if (isExternal) {
                                if (imageMicroThumbs == null) {
                                    imageMicroThumbs = MiniThumbFile.instance(
                                            Images.Media.EXTERNAL_CONTENT_URI);
                                }
                                imageMicroThumbs.eraseMiniThumb(id);
                            }
                        } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) {
                            deleteIfAllowed(uri, data);
                            MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                    volumeName, FileColumns.MEDIA_TYPE_VIDEO, id);

                            idvalue[0] = String.valueOf(id);
                            database.mNumQueries++;
                            Cursor cc = db.query("videothumbnails", sDataOnlyColumn,
                                        "video_id=?", idvalue, null, null, null);
                            try {
                                while (cc.moveToNext()) {
                                    deleteIfAllowed(uri, cc.getString(0));
                                }
                                database.mNumDeletes++;
                                db.delete("videothumbnails", "video_id=?", idvalue);
                            } finally {
                                IoUtils.closeQuietly(cc);
                            }
                            if (isExternal) {
                                if (videoMicroThumbs == null) {
                                    videoMicroThumbs = MiniThumbFile.instance(
                                            Video.Media.EXTERNAL_CONTENT_URI);
                                }
                                videoMicroThumbs.eraseMiniThumb(id);
                            }
                        } else if (mediaType == FileColumns.MEDIA_TYPE_AUDIO) {
                            if (!database.mInternal) {
                                MediaDocumentsProvider.onMediaStoreDelete(getContext(),
                                        volumeName, FileColumns.MEDIA_TYPE_AUDIO, id);

                                idvalue[0] = String.valueOf(id);
                                database.mNumDeletes += 2; // also count the one below
                                db.delete("audio_genres_map", "audio_id=?", idvalue);
                                // for each playlist that the item appears in, move
                                // all the items behind it forward by one
                                Cursor cc = db.query("audio_playlists_map",
                                            sPlaylistIdPlayOrder,
                                            "audio_id=?", idvalue, null, null, null);
                                try {
                                    while (cc.moveToNext()) {
                                        playlistvalues[0] = "" + cc.getLong(0);
                                        playlistvalues[1] = "" + cc.getInt(1);
                                        database.mNumUpdates++;
                                        db.execSQL("UPDATE audio_playlists_map" +
                                                " SET play_order=play_order-1" +
                                                " WHERE playlist_id=? AND play_order>?",
                                                playlistvalues);
                                    }
                                    db.delete("audio_playlists_map", "audio_id=?", idvalue);
                                } finally {
                                    IoUtils.closeQuietly(cc);
                                }
                            }
                        } else if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
                            // TODO, maybe: remove the audio_playlists_cleanup trigger and
                            // implement functionality here (clean up the playlist map)
                        }
                    }
                } finally {
                    IoUtils.closeQuietly(c);
                    if (imageMicroThumbs != null) {
                        imageMicroThumbs.deactivate();
                    }
                    if (videoMicroThumbs != null) {
                        videoMicroThumbs.deactivate();
                    }
                }
                // Do not allow deletion if the file/object is referenced as parent
                // by some other entries. It could cause database corruption.
                if (!TextUtils.isEmpty(tableAndWhere.where)) {
                    tableAndWhere.where =
                            "(" + tableAndWhere.where + ")" +
                                    " AND (_id NOT IN (SELECT parent FROM files" +
                                    " WHERE NOT (" + tableAndWhere.where + ")))";
                } else {
                    tableAndWhere.where = ID_NOT_PARENT_CLAUSE;
                }
            }
        }

        switch (match) {
            case MTP_OBJECTS:
            case MTP_OBJECTS_ID:
                database.mNumDeletes++;
                count = db.delete("files", tableAndWhere.where, whereArgs);
                break;
            case AUDIO_GENRES_ID_MEMBERS:
                database.mNumDeletes++;
                count = db.delete("audio_genres_map",
                        tableAndWhere.where, whereArgs);
                break;

            case IMAGES_THUMBNAILS_ID:
            case IMAGES_THUMBNAILS:
            case VIDEO_THUMBNAILS_ID:
            case VIDEO_THUMBNAILS:
                // Delete the referenced files first.
                Cursor c = db.query(tableAndWhere.table,
                        sDataOnlyColumn,
                        tableAndWhere.where, whereArgs, null, null, null);
                if (c != null) {
                    try {
                        while (c.moveToNext()) {
                            deleteIfAllowed(uri, c.getString(0));
                        }
                    } finally {
                        IoUtils.closeQuietly(c);
                    }
                }
                database.mNumDeletes++;
                count = db.delete(tableAndWhere.table,
                        tableAndWhere.where, whereArgs);
                break;

            default:
                database.mNumDeletes++;
                count = db.delete(tableAndWhere.table,
                        tableAndWhere.where, whereArgs);
                break;
        }

        // Since there are multiple Uris that can refer to the same files
        // and deletes can affect other objects in storage (like subdirectories
        // or playlists) we will notify a change on the entire volume to make
        // sure no listeners miss the notification.
        Uri notifyUri = Uri.parse("content://" + MediaStore.AUTHORITY + "/" + volumeName);
        getContext().getContentResolver().notifyChange(notifyUri, null);
    }

    return count;
}

5.5 MediaProvider 查询

public Cursor query(Uri uri, String[] projectionIn, String selection,
        String[] selectionArgs, String sort) {
    uri = safeUncanonicalize(uri);
    int table = URI_MATCHER.match(uri);
    List prependArgs = new ArrayList();
    // handle MEDIA_SCANNER before calling getDatabaseForUri()
    if (table == MEDIA_SCANNER) {
        if (mMediaScannerVolume == null) {
            return null;
        } else {
            // create a cursor to return volume currently being scanned by the media scanner
            MatrixCursor c = new MatrixCursor(
                new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
            c.addRow(new String[] {mMediaScannerVolume});
            //直接返回的是有关存储卷的cursor
            return c;
        }
    }
    // Used temporarily (until we have unique media IDs) to get an identifier
    // for the current sd card, so that the music app doesn't have to use the
    // non-public getFatVolumeId method
    if (table == FS_ID) {
        MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
        c.addRow(new Integer[] {mVolumeId});
        return c;
    }
    if (table == VERSION) {
        MatrixCursor c = new MatrixCursor(new String[] {"version"});
        c.addRow(new Integer[] {getDatabaseVersion(getContext())});
        return c;
    }
    //初始化DatabaseHelper和SQLiteDatabase
    String groupBy = null;
    DatabaseHelper helper = getDatabaseForUri(uri);
    if (helper == null) {
        return null;
    }
    helper.mNumQueries++;
    SQLiteDatabase db = null;
    try {
        db = helper.getReadableDatabase();
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    if (db == null) return null;
    // SQLiteQueryBuilder类是组成查询语句的帮助类
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    //获取uri里面的查询字符
    String limit = uri.getQueryParameter("limit");
    String filter = uri.getQueryParameter("filter");
    String [] keywords = null;
    if (filter != null) {
        filter = Uri.decode(filter).trim();
        if (!TextUtils.isEmpty(filter)) {
            //对字符进行筛选
            String [] searchWords = filter.split(" ");
            keywords = new String[searchWords.length];
            for (int i = 0; i < searchWords.length; i++) {
                String key = MediaStore.Audio.keyFor(searchWords[i]);
                key = key.replace("\\", "\\\\");
                key = key.replace("%", "\\%");
                key = key.replace("_", "\\_");
                keywords[i] = key;
            }
        }
    }
    if (uri.getQueryParameter("distinct") != null) {
        qb.setDistinct(true);
    }
    boolean hasThumbnailId = false;
    //对匹配的其他类型进行设置查询语句的操作
    switch (table) {
        case IMAGES_MEDIA:
                //设置查询的表是images
                qb.setTables("images");
                if (uri.getQueryParameter("distinct") != null)
                    //设置为唯一的
                    qb.setDistinct(true);
                break;
         //其他类型相类似
         ... ...
    }
    //根据拼装的搜索条件,进行查询
    Cursor c = qb.query(db, projectionIn, selection,
             combine(prependArgs, selectionArgs), groupBy, null, sort, limit);

    if (c != null) {
        String nonotify = uri.getQueryParameter("nonotify");
        if (nonotify == null || !nonotify.equals("1")) {
            //通知更新数据库
            c.setNotificationUri(getContext().getContentResolver(), uri);
        }
    }
    return c;
}

6. MediaProvider 如何更新数据库

站在 Java 层来看,不管是扫描具体的文件,还是扫描一个目录,最终都会走到 Java 层 MyMediaScannerClient 的 doScanFile()。在前文我们已经列出过这个函数的代码,为了说明问题,这里再列一下其中的重要句子:
/frameworks/base/media/java/android/media/MediaScanner.java

public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    try {
        // ① beginFile
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);
        ...

        // rescan for metadata if file was modified since last scan
        if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
            if (noMedia) {
                result = endFile(entry, false, false, false, false, false);
            } else {
                // 正常文件处理走到这里
                ...
                // we only extract metadata for audio and video files
                if (isaudio || isvideo) {
                    // ② processFile 这边主要是解析媒体文件的元数据,以便后续存入到数据库中
                    mScanSuccess = processFile(path, mimeType, this);
                }

                if (isimage) {
                    mScanSuccess = processImageFile(path);
                }
                ...
                // ③ endFile
                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    }
    ...
    return result;
}

重看一下其中和 MediaProvider 相关的 beginFile() 和 endFile()。

beginFile()是为了后续和MediaProvider打交道,准备一个FileEntry。FileEntry的定义如下:

private static class FileEntry {
    long mRowId;
    String mPath;
    long mLastModified;
    int mFormat;
    boolean mLastModifiedChanged;

    FileEntry(long rowId, String path, long lastModified, int format) {
        mRowId = rowId;
        mPath = path;
        mLastModified = lastModified;
        mFormat = format;
        mLastModifiedChanged = false;
    }
    ...
}

FileEntry 的几个成员变量,其实体现了查表时的若干列的值。
beginFile()的代码截选如下:

public FileEntry beginFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean noMedia) {
    ...
    FileEntry entry = makeEntryFor(path); // 从MediaProvider中查出该文件或目录对应的入口
    ...
    if (entry == null || wasModified) {
        // 不管原来表中是否存在这个路径文件数据,这里面都会执行到
        if (wasModified) {
            // 更新最后编辑时间
            entry.mLastModified = lastModified;
        } else {
            // 如果前面没查到FileEntry,就在这里new一个新的FileEntry
            entry = new FileEntry(0, path, lastModified,
                    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
        }
        entry.mLastModifiedChanged = true;
    }
    ...
    return entry;
}

其中调用的 makeEntryFor() 内部就会查询 MediaProvider:

FileEntry makeEntryFor(String path) {
    String where;
    String[] selectionArgs;

    Cursor c = null;
    try {
        where = Files.FileColumns.DATA + "=?";
        selectionArgs = new String[] { path };
        c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION,
                where, selectionArgs, null, null);
        if (c.moveToFirst()) {
            long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
            int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
            long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
            return new FileEntry(rowId, path, lastModified, format);
        }
    } catch (RemoteException e) {
    } finally {
        if (c != null) {
            c.close();
        }
    }
    return null;
}

查询语句中用的 FILES_PRESCAN_PROJECTION 的定义如下:

private static final String[] FILES_PRESCAN_PROJECTION = new String[] {
        Files.FileColumns._ID, // 0
        Files.FileColumns.DATA, // 1
        Files.FileColumns.FORMAT, // 2
        Files.FileColumns.DATE_MODIFIED, // 3
};

看到了吗,特意要去查一下 MediaProvider 中记录的待查文件的最后修改日期。能查到就返回一个 FileEntry,如果查询时出现异常就返回 null。beginFile() 的 lastModified 参数可以理解为是从文件系统里拿到的待查文件的最后修改日期,它应该是最准确的。而 MediaProvider 里记录的信息则有可能“较老”。beginFile() 内部通过比对这两个“最后修改日期”,就可以知道该文件是不是真的改动了。如果的确改动了,就要把 FileEntry 里的 mLastModified 调整成最新数据。

基本上而言,beginFile() 会返回一个 FileEntry。如果该阶段没能在MediaProvider里找到文件对应的记录,那么 FileEntry 对象的mRowId会为0,而如果找到了,则为非0值。

与 beginFile() 相对的,就是 endFile() 了。endFile() 是真正向 MediaProvider 数据库插入数据或更新数据的地方。当 FileEntry 的 mRowId 为0时,会考虑调用:

result = mMediaProvider.insert(tableUri, values);

而当 mRowId 为非0值时,则会考虑调用:

mMediaProvider.update(result, values, null, null);

这就是改变 MediaProvider 中相关信息的最核心代码。

endFile() 的代码截选如下:

private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications,
        boolean alarms, boolean music, boolean podcasts)
        throws RemoteException {
    ...
    ContentValues values = toValues();
    String title = values.getAsString(MediaStore.MediaColumns.TITLE);
    if (title == null || TextUtils.isEmpty(title.trim())) {
        title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA));
        values.put(MediaStore.MediaColumns.TITLE, title);
    }
    ...
    long rowId = entry.mRowId;
    if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) {
        // Only set these for new entries. For existing entries, they
        // may have been modified later, and we want to keep the current
        // values so that custom ringtones still show up in the ringtone
        // picker.
        values.put(Audio.Media.IS_RINGTONE, ringtones);
        values.put(Audio.Media.IS_NOTIFICATION, notifications);
        values.put(Audio.Media.IS_ALARM, alarms);
        values.put(Audio.Media.IS_MUSIC, music);
        values.put(Audio.Media.IS_PODCAST, podcasts);
    } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
            || mFileType == MediaFile.FILE_TYPE_HEIF
            || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
        ...
    }
    ...
    if (rowId == 0) {
        // 扫描的是新文件,insert记录。如果是目录的话,必须比它所含有的所有文件更早插入记录,
        // 所以在批量插入时,就需要有更高的优先权。如果是文件的话,而且我们现在就需要其对应
        // 的rowId,那么应该立即进行插入,此时不过多考虑批量插入。
        // New file, insert it.
        // Directories need to be inserted before the files they contain, so they
        // get priority when bulk inserting.
        // If the rowId of the inserted file is needed, it gets inserted immediately,
        // bypassing the bulk inserter.
        if (inserter == null || needToSetSettings) {
            if (inserter != null) {
                inserter.flushAll();
            }
            result = mMediaProvider.insert(tableUri, values);
        } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) {
            inserter.insertwithPriority(tableUri, values);
        } else {
            inserter.insert(tableUri, values);
        }

        if (result != null) {
            rowId = ContentUris.parseId(result);
            entry.mRowId = rowId;
        }
    } else {
        ...
        mMediaProvider.update(result, values, null, null);
    }
    ...
    return result;
}

除了直接调用 mMediaProvider.insert() 向 MediaProvider 中写入数据,函数中还有一种方式是经由 inserter 对象,其类型为 MediaInserter。

MediaInserter 也是向 MediaProvider 中写入数据,最终大体上会走到其 flush() 函数,该函数的代码如下:

private void flush(Uri tableUri, List list) throws RemoteException {
    if (!list.isEmpty()) {
        ContentValues[] valuesArray = new ContentValues[list.size()];
        valuesArray = list.toArray(valuesArray);
        mProvider.bulkInsert(tableUri, valuesArray);
        list.clear();
    }
}

参考

Android-MediaScanner&MediaProvider学习
Android MediaScanner
Android扫描多媒体文件剖析
MediaScannerService研究
android_9.0 MediaScanner 媒体扫描详解
Android 多媒体扫描 MediaScannerConnection
Android多媒体总纲
MediaProvider流程分析
多媒体文件管理-数据库external.db,internal.db (一)

你可能感兴趣的:(Android MediaProvider)