MediaScannerService研究

MediaScannerService研究

侯 亮
(本文以Android 5.1为准)

1 概述

MediaScannerService是Android平台提供的一个用于扫描手机中多媒体文件的应用级service。它并不是系统服务。MediaScannerService和MediaProvider有着非常紧密的关系,因为扫描出的结果总需要存储到某个地方来展现给用户。那么它们具体是如何结合的呢?本文将逐步加以阐述。

我们先来初步了解一下MediaScannerService,它在AndroidManifest.xml文件里的相关信息如下:
【packages/providers/mediaprovider/AndroidManifest.xml】

1<service android:name="MediaScannerService" android:exported="true"> 2    <intent-filter> 3        <action android:name="android.media.IMediaScannerService" /> 4    </intent-filter> 5</service>

MediaScannerService本身继承于Service,而且还实现了Runnable接口。其定义截选如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

1public class MediaScannerService extends Service implements Runnable 2{ 3    private static final String TAG = "MediaScannerService"; 4 5    private volatile Looper             mServiceLooper; 6    private volatile ServiceHandler     mServiceHandler; 7    private PowerManager.WakeLock         mWakeLock; 8    private String[]                     mExternalStoragePaths; 9    . . . . . . 10    private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub()  11            . . . . . . 12    . . . . . . 13}

1.1 在onCreate()中启动工作线程

MediaScannerService的onCreate()函数如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

1@Override 2public void onCreate() 3{ 4    PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 5    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 6StorageManager storageManager =  7                  (StorageManager)getSystemService(Context.STORAGE_SERVICE); 8    mExternalStoragePaths = storageManager.getVolumePaths(); 9 10    // 启动最重要的工作线程,该线程也是个消息泵线程 11    Thread thr = new Thread(null, this, "MediaScannerService"); 12    thr.start(); 13}

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

1public void run() 2{ 3    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + 4                                   Process.THREAD_PRIORITY_LESS_FAVORABLE); 5    Looper.prepare(); 6 7    mServiceLooper   = Looper.myLooper();            // 消息looper 8    mServiceHandler  = new ServiceHandler();        // 发送消息的handler 9 10    Looper.loop(); 11}

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

1.2 向工作线程发送Message

比较常见的向消息泵发送Message的做法是调用startService(),并在MediaScannerService的onStartCommand()函数里sendMessage()。比如,和MediaScannerService配套提供的MediaScannerReceiver,当它收到类似ACTION_BOOT_COMPLETED这样的系统广播时,就会调用自己的scan()或scanFile()函数。而scan()函数的代码如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerReceiver.java】

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

startService()动作会导致走到service的onStartCommand(),并进一步发送消息,其函数截选如下:

1@Override 2public int onStartCommand(Intent intent, int flags, int startId) 3{ 4    . . . . . . 5    . . . . . . 6    Message msg = mServiceHandler.obtainMessage(); 7    msg.arg1 = startId; 8    msg.obj = intent.getExtras(); 9    mServiceHandler.sendMessage(msg);    // 发送消息! 10 11    // Try again later if we are killed before we can finish scanning. 12    return Service.START_REDELIVER_INTENT; 13}

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

IMediaScannerService接口只提供了两个接口函数:

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

处理这两种请求的实体是服务内部的mBinder对象,参考代码如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

1private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() { 2public void requestScanFile(String path, String mimeType,  3                                  IMediaScannerListener listener) 4    { 5        Bundle args = new Bundle(); 6        args.putString("filepath", path); 7        args.putString("mimetype", mimeType); 8        if (listener != null) { 9            args.putIBinder("listener", listener.asBinder()); 10        } 11        startService(new Intent(MediaScannerService.this, 12                                     MediaScannerService.class).putExtras(args)); 13    } 14 15    public void scanFile(String path, String mimeType) { 16        requestScanFile(path, mimeType, null); 17    } 18};

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

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

1private final class ServiceHandler extends Handler 2{ 3    @Override 4    public void handleMessage(Message msg) 5    { 6        Bundle arguments = (Bundle) msg.obj; 7        String filePath = arguments.getString("filepath"); 8        . . . . . . 9        if (filePath != null) { 10            . . . . . . 11                uri = scanFile(filePath, arguments.getString("mimetype")); 12            . . . . . . 13        } else { 14            . . . . . . 15                scan(directories, volume); 16            . . . . . . 17        } 18        . . . . . . 19        stopSelf(msg.arg1); 20    } 21};

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

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

2 运作细节

2.1 发起扫描动作

现在我们已经了解了,要发起扫描动作,大体上只有两种方式:
1)用广播来发起扫描动作;
2)绑定服务来发起扫描动作;
下面我们细说一下这两种方式。

2.1.1 用广播来发起扫描动作

扫描服务的配套receiver是MediaScannerReceiver,它在AndroidManifest.xml里的描述如下:

1<receiver android:name="MediaScannerReceiver"> 2    <intent-filter> 3        <action android:name="android.intent.action.BOOT_COMPLETED" /> 4    </intent-filter> 5    <intent-filter> 6        <action android:name="android.intent.action.MEDIA_MOUNTED" /> 7        <data android:scheme="file" /> 8    </intent-filter> 9    <intent-filter> 10        <action android:name="android.intent.action.MEDIA_UNMOUNTED" /> 11        <data android:scheme="file" /> 12    </intent-filter> 13    <intent-filter> 14        <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" /> 15        <data android:scheme="file" /> 16    </intent-filter> 17</receiver>

MediaScannerReceiver的onReceive()代码如下:

1public void onReceive(Context context, Intent intent) { 2    final String action = intent.getAction(); 3    final Uri uri = intent.getData(); 4     5    if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { 6        // Scan both internal and external storage 7        scan(context, MediaProvider.INTERNAL_VOLUME);  // INTERNAL_VOLUME = "internal" 8        scan(context, MediaProvider.EXTERNAL_VOLUME);  // EXTERNAL_VOLUME = "external" 9    } else { 10        if (uri.getScheme().equals("file")) { 11            // handle intents related to external storage 12            . . . . . . 13 14            Log.d(TAG, "action: " + action + " path: " + path); 15            if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) { 16                // scan whenever any volume is mounted 17                scan(context, MediaProvider.EXTERNAL_VOLUME); 18            } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) && 19                    path != null && path.startsWith(externalStoragePath + "/")) { 20                scanFile(context, path); 21            } 22        } 23    } 24}
  • 当系统刚刚启动时,收到ACTION_BOOT_COMPLETED广播,此时会把内部卷标(“internal”)和外部卷标(“external”)都扫描一下;
  • 如果收到ACTION_MEDIA_MOUNTED广播,则只扫描外部卷标;
  • 如果收到的是ACTION_MEDIA_SCANNER_SCAN_FILE广播,则扫描具体的文件路径。

当用户插入了扩展介质(一般指SD卡),并且该介质已经被系统正确识别、安装,系统就会发出ACTION_MEDIA_MOUNTED广播。从Android 4.4开始,ACTION_MEDIA_MOUNTED广播只能由系统(系统服务MountService)发出,普通用户是无权发送的。

另外,我们可以通过发送ACTION_MEDIA_SCANNER_SCAN_FILE广播,要求MediaScannerService扫描一下具体的文件。比如说在ExternalStorageProvider的openDocument()函数里,就会设置监听器监听用户是不是在读写模式下close了某个文件,因为close一般表示写入动作已经完成了,那么此时就需要“踢一下”MediaScannerService,让它更新一下自己的数据。这段代码截选如下:
【frameworks/base/packages/externalstorageprovider/src/com/android/externalstorage/ExternalStorageProvider.java】

1@Override 2public ParcelFileDescriptor openDocument(String documentId, String mode,  3                                                 CancellationSignal signal) 4                                                 throws FileNotFoundException  5{ 6    . . . . . . 7            // When finished writing, kick off media scanner 8            return ParcelFileDescriptor.open(file, pfdMode, mHandler,  9                                                    new OnCloseListener() { 10                @Override 11                public void onClose(IOException e) { 12                    final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 13                    intent.setData(Uri.fromFile(file)); 14                    getContext().sendBroadcast(intent);    // 用广播来发起扫描动作 15                } 16            }); 17    . . . . . . 18}

2.1.2 用MediaScannerConnection来发起扫描动作

除了利用类似ACTION_MEDIA_SCANNER_SCAN_FILE这样的广播,系统中还有一种办法可以发起扫描动作,那就是先利用bindService机制得到的IMediaScannerService代理接口,而后再通过调用该接口的requestScanFile()或scanFile(),同样可以向MediaScannerService发出扫描语义。

不过,我们一般并不直白地去bindService,而是通过一种封装好的辅助类:MediaScannerConnection。该类的定义截选如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

1public class MediaScannerConnection implements ServiceConnection { 2    private static final String TAG = "MediaScannerConnection"; 3    private Context mContext; 4    private MediaScannerConnectionClient mClient; 5    private IMediaScannerService mService; 6    private boolean mConnected; // true if connect() has been called since last disconnect() 7    private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() 8    . . . . . .

请注意那个mService成员,它就是为了绑定service而设计的。

MediaScannerConnection里设计了两个scanFile()函数,一个动态的,一个静态的。大家不要搞混了。

2.1.2.1 动态形式scanFile()

动态形式scanFile()的代码截选:

1public void scanFile(String path, String mimeType) { 2    . . . . . . 3            mService.requestScanFile(path, mimeType, mListener); 4    . . . . . . 5}

对于动态形式的scanFile()而言,它只能在MediaScannerConnection成功绑定到MediaScannerService之后调用,此时它简单地调用mService.requestScanFile()将语义传递给MediaScannerService,再由MediaScannerService通过startService()向自己的消息泵线程打入消息。

mService.requestScanFile()的最后一个参数mListener的定义如下:

1private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() { 2    public void scanCompleted(String path, Uri uri) { 3        MediaScannerConnectionClient client = mClient; 4        if (client != null) { 5            client.onScanCompleted(path, uri); 6        } 7    } 8};

它是个简单的binder实体。每当MediaScannerService扫描完所指定的一个文件后,就会回调到该实体的scanCompleted()。此时一般会经由client.onScanCompleted()一句间接调用下一次scanFile()的动作,从而使扫描多个文件的动作连贯起来。

2.1.2.2 静态形式scanFile()

静态形式scanFile()的代码截选:

1public static void scanFile(Context context, String[] paths, String[] mimeTypes, 2                            OnScanCompletedListener callback) { 3    ClientProxy client = new ClientProxy(paths, mimeTypes, callback); 4    MediaScannerConnection connection = new MediaScannerConnection(context, client); 5    client.mConnection = connection; 6    connection.connect();  // 内部主要是bindService动作 7}

对于静态形式的scanFile()而言,会重新创建一个MediaScannerConnection对象,并通过connect()动作和MediaScannerService联系起来。

请大家注意创建MediaScannerConnection时传入的第二个参数client,它必须实现MediaScannerConnectionClient接口。说穿了是为了监听两种事情:
1)和MediaScannerService之间的连接是否建立好了;
2)MediaScannerService中扫描某文件的动作是否执行完了;
     
MediaScannerConnectionClient接口的定义如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

1public interface MediaScannerConnectionClient extends OnScanCompletedListener { 2    public void onMediaScannerConnected(); 3    public void onScanCompleted(String path, Uri uri); 4}

在静态形式的scanFile()中,实现MediaScannerConnectionClient接口的类是ClientProxy,它是这样实现onMediaScannerConnected()和onScanCompleted()的:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

1public void onMediaScannerConnected() { 2    scanNextPath(); 3} 4 5public void onScanCompleted(String path, Uri uri) { 6    if (mClient != null) { 7        mClient.onScanCompleted(path, uri); 8    } 9    scanNextPath(); 10}

可以看到一旦连接建立成功或者某个文件扫描完毕,就会调用scanNextPath(),进一步扫描接下来的内容,直到把调用静态scanFile()时传入的paths数组遍历完毕。

1void scanNextPath() { 2    if (mNextPath >= mPaths.length) { 3        mConnection.disconnect(); 4        return; 5    } 6    String mimeType = mMimeTypes != null ? mMimeTypes[mNextPath] : null; 7    mConnection.scanFile(mPaths[mNextPath], mimeType); 8    mNextPath++; 9}

实际上,MediaScannerConnection的connect()动作就是在bindService(),它的代码如下:
【frameworks/base/media/java/android/media/MediaScannerConnection.java】

1public void connect() { 2    synchronized (this) { 3        if (!mConnected) { 4            Intent intent = new Intent(IMediaScannerService.class.getName()); 5            intent.setComponent( new ComponentName("com.android.providers.media", 6                                        "com.android.providers.media.MediaScannerService")); 7            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); 8            mConnected = true; 9        } 10    } 11}

因为bindService()动作本身是异步的,初始时mService的值还是null,所以我们不能直接在这里执行类似mService.requestScanFile()这样的操作。我们必须等到bind动作成功完成,系统回调到MediaScannerConnection的onServiceConnected(),才会给mService赋值:

1public void onServiceConnected(ComponentName className, IBinder service) { 2    . . . . . . 3    synchronized (this) { 4        mService = IMediaScannerService.Stub.asInterface(service); 5        if (mService != null && mClient != null) { 6            mClient.onMediaScannerConnected(); 7        } 8    } 9}

如果bind动作是成功的,而且用户在构造MediaScannerConnection对象时传入了client参数。那么此时就会回调mClient的onMediaScannerConnected()函数。

请注意,静态的scanFile()方法最终并没有直接执行requestScanFile(),它先建立了和MediaScannerService的绑定关系,然后在onServiceConnected()中感知到绑定已经成功之后,才会经由ClientProxy间接转过头调用到自己的scanFile()函数,从而执行到requestScanFile()。

ClientProxy、MediaScannerConnection、MediaScannerService三者之间的关系如下图所示:

以MediaScannerConnection对象为桥梁:
1)其mService“指向”MediaScannerService的mBinder;
2)其mClient指向ClientProxy对象;

当然,在看懂上图后,我们也可以不使用默认的ClientProxy,而添加我们自定义的client对象,只要这个client对象实现了MediaScannerConnectionClient接口即可。比如在MediaProvider中,就定义了另一个类ScannerClient类,代码截选如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaProvider.java】

1private static final class ScannerClient implements MediaScannerConnectionClient { 2    String mPath = null; 3    MediaScannerConnection mScannerConnection; 4    SQLiteDatabase mDb; 5 6    public ScannerClient(Context context, SQLiteDatabase db, String path) { 7        mDb = db; 8        mPath = path; 9        mScannerConnection = new MediaScannerConnection(context, this); 10        mScannerConnection.connect(); 11    } 12 13    @Override 14    public void onMediaScannerConnected() { 15        . . . . . . 16    } 17 18    @Override 19    public void onScanCompleted(String path, Uri uri) { 20    } 21}

这么看来,MediaScannerConnection还真是起连接作用的“connection”,它将发起扫描请求的client和最终执行扫描动作的MediaScannerService连接起来了。我们把上面那张图简化一下,可以看到如下示意图:

以上介绍的就是发起scan动作的方法,接下来我们来看看到底有哪些地方在使用这些方法。

2.2 谁会发起扫描动作

2.2.1 发起者列表

发出ACTION_MEDIA_SCANNER_SCAN_FILE广播的地方:

发起方

相关代码位置

说明

ExternalStorageProvider

openDocument()注册OnCloseListener的地方

 

ComposeMessageActivity

MMS里copyPart()函数中

saveRingtone()、
copyMedia()中都会调用copyPart()。

DownloadProvider

openFile()注册OnCloseListener的地方

 

EmlAttachmentProvider

copyAttachment(),将附件拷到外部下载目录(一般是SD卡)时 

provider在update()中处理ATTACHMENT的地方

SoundRecorder

addToMediaDB() 

录制sample后,要添加进多媒体数据库

利用MediaScannerConnection的地方:

发起方

相关代码位置

说明

AttachmentUtilities

saveAttachment()

代码截选见下文

BeamTransferManager

processFiles()

NFC方面,
finishTransfer()、handleMessage()处理MSG_NEXT_TRANSFER_TIMER时,都会调用processFiles()。

BluetoothOppService

MediaScannerNotifier

没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的MediaScannerNotifier

CalendarDebugActivity

doInBackground() 

DumpDbTask的doInBackground(),将数据库文件存成calendar.db.zip之后,调用MediaScannerConnection.scanFile()

DownloadScanner

DownloadScanner

没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的DownloadScanner

FmRecorder

addRecordingToDatabase()

MediaScannerConnection.scanFile(context, 
new String[] { mRecordFile.getPath() },
                null, null);

IngestService

ScannerClient 

没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的ScannerClient

MediaProvider

ScannerClient 

没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的ScannerClient

VCardService

CustomeMediaScannerConnectionClient

没有直接使用MediaScannerConnection.scanFile(),而是编写了自己的CustomeMediaScannerConnectionClient

2.2.2 saveAttachment()中的示例代码

我们举一个实际的例子。在Email模块中,如果附件存入了外部存储器,那么就有必要扫描一次媒体文件了,这样才能够立即将相关文件体现到Gallery、Music中。所以在saveAttachment()函数里,就会调用MediaScannerConnection.scanFile():
【packages/apps/email/emailcommon/src/com/android/emailcommon/utility/AttachmentUtilities.java】

1public static void saveAttachment(Context context, InputStream in, Attachment attachment) { 2    . . . . . . 3        ContentResolver resolver = context.getContentResolver(); 4        if (attachment.mUiDestination == UIProvider.AttachmentDestination.CACHE) { 5            . . . . . . 6        } else if (Utility.isExternalStorageMounted()) { 7            . . . . . . 8            File file = Utility.createUniqueFile(downloads, attachment.mFileName); 9            size = copyFile(in, new FileOutputStream(file)); 10            String absolutePath = file.getAbsolutePath(); 11 12            // 尽管下载管理器会扫描媒体文件,但只会在用户运行download APP并点击相关按钮后, 13            // 才会进行扫描。所以,我们自己运行一下media scanner,以便把附件立即添加进gallery / music。 14            MediaScannerConnection.scanFile(context, new String[] {absolutePath},  15                                                  null, null); 16            . . . . . . 17                DownloadManager dm = (DownloadManager)  18                                      context.getSystemService(Context.DOWNLOAD_SERVICE); 19                long id = dm.addCompletedDownload(attachment.mFileName,  20                        attachment.mFileName, 21                        false /* do not use media scanner */, 22                        mimeType, absolutePath, size, 23                        true /* show notification */); 24                contentUri = dm.getUriForDownloadedFile(id).toString(); 25            . . . . . . 26        } else { 27            . . . . . . 28            throw new IOException(); 29        } 30    . . . . . . 31    context.getContentResolver().update(uri, cv, null, null); 32}

2.3 说说实际的扫描动作

前文介绍MediaScannerService的消息泵线程时已经说过,最终ServiceHandler的handleMessage()会调用scanFile()或scan()来完成扫描。现在我们来看看scanFile()、scan()的细节。

2.3.1 scanFile()动作

MediaScannerService的scanFile()定义如下:
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

1private Uri scanFile(String path, String mimeType) { 2    String volumeName = MediaProvider.EXTERNAL_VOLUME; 3    openDatabase(volumeName); 4    MediaScanner scanner = createMediaScanner(); 5    try { 6        String canonicalPath = new File(path).getCanonicalPath(); 7        return scanner.scanSingleFile(canonicalPath, volumeName, mimeType); 8    } catch (Exception e) { 9        Log.e(TAG, "bad path " + path + " in scanFile()", e); 10        return null; 11    } 12}

可以看到,scanFile()函数内部借助了辅助类MediaScanner,调用了该类的scanSingleFile()。这个MediaScanner才是重头戏,它的scanSingleFile()代码截选如下: 
【frameworks/base/media/java/android/media/MediaScanner.java】

1public Uri scanSingleFile(String path, String volumeName, String mimeType) { 2    . . . . . . 3        initialize(volumeName); 4        prescan(path, true); 5        File file = new File(path); 6        . . . . . . 7        // always scan the file, so we can return the content://media Uri for existing files 8        return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 9                false, true, MediaScanner.isNoMediaPath(path)); 10    . . . . . . 11}

借助了mClient.doScanFile()。

此处的mClient类型为MyMediaScannerClient,mClient的定义是:

private final MyMediaScannerClient mClient = new MyMediaScannerClient();

MyMediaScannerClient类的doScanFile()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1public Uri doScanFile(String path, String mimeType, long lastModified, 2        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 3    . . . . . . 4        FileEntry entry = beginFile(path, mimeType, lastModified, 5                fileSize, isDirectory, noMedia); 6        . . . . . . 7        if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 8            if (noMedia) { 9                result = endFile(entry, false, false, false, false, false); 10            } else { 11                . . . . . . 12                . . . . . . 13                // we only extract metadata for audio and video files 14                if (isaudio || isvideo) { 15                    processFile(path, mimeType, this); 16                } 17                if (isimage) { 18                    processImageFile(path); 19                } 20 21                result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 22            } 23        } 24    . . . . . . 25    return result; 26}

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

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

2.3.2 scan()动作

与scanFile()动作类似,MediaScannerService中扫描目录的动作是scan():
【packages/providers/mediaprovider/src/com/android/providers/media/MediaScannerService.java】

1private void scan(String[] directories, String volumeName) { 2. . . . . . 3    values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName); 4    Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values); 5    . . . . . . 6            MediaScanner scanner = createMediaScanner(); 7            scanner.scanDirectories(directories, volumeName); 8    . . . . . . 9        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri)); 10    . . . . . . 11}

同样是借助了辅助类MediaScanner,调用了该类的scanDirectories()。

scanDirectories()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1public void scanDirectories(String[] directories, String volumeName) { 2    . . . . . .  3        for (int i = 0; i < directories.length; i++) { 4            processDirectory(directories[i], mClient); 5        } 6    . . . . . . 7}

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

2.3.3 MediaScanner

顾名思义,MediaScanner就是个“媒体文件扫描器”。它必须打通java层次和C++层次。请大家注意它的两个native函数:native_init()和native_setup(),以及两个重要成员变量:一个是上文刚刚提到的mClient成员,另一个是mNativeContext。

MediaScanner的相关代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1public class MediaScanner 2{ 3    static { 4        System.loadLibrary("media_jni"); 5        native_init();    // 将java层和c++层联系起来 6 } 7    . . . . . . 8    private long mNativeContext; 9    . . . . . . 10    public MediaScanner(Context c) { 11        native_setup(); 12        . . . . . . 13    } 14 . . . . . . 15    // 一开始就具有明确的mClient对象 16    private final MyMediaScannerClient mClient = new MyMediaScannerClient(); 17    . . . . . . 18}

MediaScanner类加载之时,就会同时加载动态链接库“media_jni”,并调用native_init()将java层和c++层联系起来。而且MediaScanner对象一开始就具有明确的mClient对象,类型为MyMediaScannerClient。

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

1static void 2android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz) 3{ 4    ALOGV("native_setup"); 5    MediaScanner *mp = new StagefrightMediaScanner; 6    if (mp == NULL) { 7        jniThrowException(env, kRunTimeException, "Out of memory"); 8        return; 9    } 10    env->SetLongField(thiz, fields.context, (jlong)mp); 11}

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

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

2.3.4 调用到C++层次

不管是扫描文件,还是扫描目录,总之MediaScannerService已经把工作委托给MediaScanner的scanSingleFile()和scanDirectories()了,而这两个函数到头来都是调用MediaScanner自己的native函数,即processFile()和processDirectory()。其声明如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1private native void processDirectory(String path, MediaScannerClient client); 2private native void processFile(String path, String mimeType, MediaScannerClient client);

MediaScanner中调用的processFile()对应于C++层的android_media_MediaScanner_processFile()。代码截选如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

1static void android_media_MediaScanner_processFile( 2                JNIEnv *env, jobject thiz, jstring path, 3                jstring mimeType, jobject client) 4{ 5    . . . . . . 6    MediaScanner *mp = getNativeScanner_l(env, thiz); 7    . . . . . . 8    const char *mimeTypeStr = 9        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL); 10    if (mimeType && mimeTypeStr == NULL) {  // Out of memory 11        // ReleaseStringUTFChars can be called with an exception pending. 12        env->ReleaseStringUTFChars(path, pathStr); 13        return; 14    } 15 16    MyMediaScannerClient myClient(env, client);        // 构造一个临时的myClient 17    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient); 18    if (result == MEDIA_SCAN_RESULT_ERROR) { 19        ALOGE("An error occurred while scanning file '%s'.", pathStr); 20    } 21    . . . . . . 22}

注意这里构造了一个局部的(C++层次)MyMediaScannerClient对象,构造myClient时传入的client参数来自于Java层调用processFile()时传入的那个(Java层次)MyMediaScannerClient对象。这个对象会记录在C++层MyMediaScannerClient的mClient域中,这个在前面的示意图中已有表示。

相应的,processDirectory()对应于C++层的android_media_MediaScanner_processDirectory()。代码截选如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

1static void android_media_MediaScanner_processDirectory( 2        JNIEnv *env, jobject thiz, jstring path, jobject client) 3{ 4    . . . . . . 5    MediaScanner *mp = getNativeScanner_l(env, thiz); 6    . . . . . . 7    MyMediaScannerClient myClient(env, client); 8    MediaScanResult result = mp->processDirectory(pathStr, myClient); 9    . . . . . . 10}

2.3.4.1 processFile()

android_media_MediaScanner_processFile()函数中的那个mp是经由下面这句得到的:

MediaScanner *mp = getNativeScanner_l(env, thiz);

它指向的其实就是StagefrightMediaScanner,所以这里调用的processFile就是:
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

1MediaScanResult StagefrightMediaScanner::processFile( 2        const char *path, const char *mimeType, 3        MediaScannerClient &client) { 4    ALOGV("processFile '%s'.", path); 5 6    client.setLocale(locale()); 7    client.beginFile(); 8    MediaScanResult result = processFileInternal(path, mimeType, client); 9    client.endFile(); 10    return result; 11}

主要行为在processFileInternal()里:
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

1MediaScanResult StagefrightMediaScanner::processFileInternal( 2        const char *path, const char * /* mimeType */, 3        MediaScannerClient &client) { 4    const char *extension = strrchr(path, '.'); 5    . . . . . . 6    if (!FileHasAcceptableExtension(extension)) { 7        return MEDIA_SCAN_RESULT_SKIPPED; 8    } 9 10    if (!strcasecmp(extension, ".mid") 11            || !strcasecmp(extension, ".smf") 12            || !strcasecmp(extension, ".imy") 13            . . . . . . 14        return HandleMIDI(path, &client); 15    } 16 17    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever); 18    int fd = open(path, O_RDONLY | O_LARGEFILE); 19    . . . . . . 20        status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL); 21        close(fd); 22    . . . . . . 23 24    const char *value; 25    if ((value = mRetriever->extractMetadata( 26                    METADATA_KEY_MIMETYPE)) != NULL) { 27        status = client.setMimeType(value); 28        . . . . . . 29    } 30 31    struct KeyMap { 32        const char *tag; 33        int key; 34    }; 35    static const KeyMap kKeyMap[] = { 36        { "tracknumber", METADATA_KEY_CD_TRACK_NUMBER }, 37        { "discnumber", METADATA_KEY_DISC_NUMBER }, 38        { "album", METADATA_KEY_ALBUM }, 39        { "artist", METADATA_KEY_ARTIST }, 40        . . . . . . 41    }; 42    static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]); 43    for (size_t i = 0; i < kNumEntries; ++i) { 44        const char *value; 45        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) { 46            status = client.addStringTag(kKeyMap[i].tag, value); 47            . . . . . . 48        } 49    } 50    return MEDIA_SCAN_RESULT_OK; 51}

可以看到,processFileInternal()里扫描具体文件的大体流程,无非是先获取多媒体文件的元数据,然后再通过MyMediaScannerClient将元数据信息从C++层传递到Java层。

processFileInternal()里的主要细节有:

1)调用FileHasAcceptableExtension()函数,看看文件的扩展名是不是属于多媒体文件扩展名,合适的扩展名有:

【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

1static bool FileHasAcceptableExtension(const char *extension) { 2    static const char *kValidExtensions[] = { 3        ".mp3", ".mp4", ".m4a", ".3gp", ".3gpp", ".3g2", ".3gpp2", 4        ".mpeg", ".ogg", ".mid", ".smf", ".imy", ".wma", ".aac", 5        ".wav", ".amr", ".midi", ".xmf", ".rtttl", ".rtx", ".ota", 6        ".mkv", ".mka", ".webm", ".ts", ".fl", ".flac", ".mxmf", 7        ".avi", ".mpeg", ".mpg", ".awb", ".mpga" 8    }; 9    . . . . . . 10}

如果扩展名不合适,则直接return MEDIA_SCAN_RESULT_SKIPPED。

2)看看文件是不是midi文件,如果是midi文件,则以HandleMIDI()来处理。
【frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp】

1    if (!strcasecmp(extension, ".mid") 2            || !strcasecmp(extension, ".smf") 3            || !strcasecmp(extension, ".imy") 4            || !strcasecmp(extension, ".midi") 5            || !strcasecmp(extension, ".xmf") 6            || !strcasecmp(extension, ".rtttl") 7            || !strcasecmp(extension, ".rtx") 8            || !strcasecmp(extension, ".ota") 9            || !strcasecmp(extension, ".mxmf")) { 10        return HandleMIDI(path, &client); 11 }

从HandleMIDI()的代码看,要解析并提取midi文件的元数据,需要用到一种EAS引擎,利用EAS_ParseMetaData()解析出时长信息。并调用MyMediaScannerClient的addStringTag()。

3)如果是其他支持的多媒体文件,则利用工具类MediaMetadataRetriever来获取文件的元数据,并将得到的元数据传递给MyMediaScannerClient。
其实MediaMetadataRetriever内部是利用系统服务“media.player”来解析多媒体文件的,这个系统服务对应的代理接口是IMediaPlayerService,它有个成员函数createMetadataRetriever()可以用于获取IMediaMetadataRetriever接口,而后就可以调用该接口的setDataSource()和extractMetadata()了。

processFileInternal()里主要通过两个函数,向Java层的MyMediaScannerClient传递数据,一个是setMimeType(),另一个是addStringTag()。以C++层的setMimeType()为例,其代码如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

1virtual status_t setMimeType(const char* mimeType) 2{ 3    ALOGV("setMimeType: %s", mimeType); 4    jstring mimeTypeStr; 5    if ((mimeTypeStr = mEnv->NewStringUTF(mimeType)) == NULL) { 6        mEnv->ExceptionClear(); 7        return NO_MEMORY; 8    } 9 10    mEnv->CallVoidMethod(mClient, mSetMimeTypeMethodID, mimeTypeStr); 11    mEnv->DeleteLocalRef(mimeTypeStr); 12    return checkAndClearExceptionFromCallback(mEnv, "setMimeType"); 13}

基本上只是通过JNI技术,调用到Java层的setMimeType()而已。

现在我们画一张关于扫描文件的简单示意图,来整理一下思路。大家顺着箭头看图就可以了。

2.3.4.2 processDirectory()

按理说,和processFile()类似,processDirectory()最终对应的代码也应该在StagefrightMediaScanner里,但是StagefrightMediaScanner并没有编写这个函数,又因为StagefrightMediaScanner继承于MediaScanner(C++层次),所以实际上使用的是MediaScanner的ProcessDirectory()
【frameworks/av/media/libmedia/MediaScanner.cpp】

1MediaScanResult MediaScanner::processDirectory( 2        const char *path, MediaScannerClient &client) { 3    int pathLength = strlen(path); 4    . . . . . . 5    char* pathBuffer = (char *)malloc(PATH_MAX + 1); 6    . . . . . . 7    strcpy(pathBuffer, path); 8    . . . . . . 9    client.setLocale(locale()); 10    MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false); 11    free(pathBuffer); 12    return result; 13}

【frameworks/av/media/libmedia/MediaScanner.cpp】

1MediaScanResult MediaScanner::doProcessDirectory(char *path, int pathRemaining,  2                                                 MediaScannerClient &client, bool noMedia) { 3    char* fileSpot = path + strlen(path); 4    struct dirent* entry; 5 6    if (shouldSkipDirectory(path)) { 7        . . . . . . 8        return MEDIA_SCAN_RESULT_OK; 9    } 10 11    // Treat all files as non-media in directories that contain a  ".nomedia" file 12    if (pathRemaining >= 8 /* strlen(".nomedia") */ ) { 13        strcpy(fileSpot, ".nomedia"); 14        if (access(path, F_OK) == 0) { 15            ALOGV("found .nomedia, setting noMedia flag"); 16            noMedia = true; 17        } 18        . . . . . . 19    } 20 21    DIR* dir = opendir(path); 22    . . . . . . 23    MediaScanResult result = MEDIA_SCAN_RESULT_OK; 24    while ((entry = readdir(dir))) { 25        if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot) 26                == MEDIA_SCAN_RESULT_ERROR) { 27            result = MEDIA_SCAN_RESULT_ERROR; 28            break; 29        } 30    } 31    closedir(dir); 32    return result; 33}

doProcessDirectory()先判断需要扫描的目录是不是应该“跳过”的目录,如果是的话,则直接return MEDIA_SCAN_RESULT_OK。判断函数shouldSkipDirectory()的代码如下:
【frameworks/av/media/libmedia/MediaScanner.cpp】

1bool MediaScanner::shouldSkipDirectory(char *path) { 2    if (path && mSkipList && mSkipIndex) { 3        int len = strlen(path); 4        int idx = 0; 5        int startPos = 0; 6        while (mSkipIndex[idx] != -1) { 7            if ((len == mSkipIndex[idx]) 8                && (strncmp(path, &mSkipList[startPos], len) == 0)) { 9                return true; 10            } 11            startPos += mSkipIndex[idx] + 1; // extra char for the delimiter 12            idx++; 13        } 14    } 15    return false; 16}

其实就是比对一下“需要扫描的目录”是否存在于mSkipList列表中。这个列表的内容其实来自于“testing.mediascanner.skiplist”属性,该属性可以记录若干目录名,目录名之间以逗号分隔。在C++层的MediaScanner构造函数中,会调用loadSkipList()来读取这个属性,解析属性中记录的所有目录名并写入mSkipList列表。

接着doProcessDirectory()用一个while循环多次调用doProcessDirectoryEntry(),其内部在必要时候,会再次调用doProcessDirectory()分析子目录。while语句的循环判断部分用到了readdir()函数,readdir()是linux上返回所指目录中“下一个进入点”(next entry)的函数,我们常常在一个while循环中调用它,以便遍历出目录中的所有内容。

doProcessDirectoryEntry()函数的定义截选如下:
【frameworks/av/media/libmedia/MediaScanner.cpp】

1MediaScanResult MediaScanner::doProcessDirectoryEntry( 2        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia, 3        struct dirent* entry, char* fileSpot) { 4    struct stat statbuf; 5    const char* name = entry->d_name; 6 7 . . . . . . 8    int type = entry->d_type; 9    . . . . . . 10    if (type == DT_DIR) {   // 普通目录 11        . . . . . . 12        if (stat(path, &statbuf) == 0) { 13            status_t status = client.scanFile(path, statbuf.st_mtime, 0, 14                    true /*isDirectory*/, childNoMedia); 15            . . . . . . 16        } 17 18        // and now process its contents 19        strcat(fileSpot, "/"); 20        MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1, 21                client, childNoMedia); 22        . . . . . . 23    } else if (type == DT_REG) {    // 普通文件 24        stat(path, &statbuf); 25        status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size, 26                false /*isDirectory*/, noMedia); 27        . . . . . . 28    } 29    return MEDIA_SCAN_RESULT_OK; 30}

不管当前处理的入口类型是“目录”还是“文件”,最终都是依靠client的scanFile()来处理,只不过前者倒数第二个参数(isDirectory)为true,后者为false而已。

client.scanFile()最终也是要调回到Java层的,MyMediaScannerClient的scanFile()代码截选如下:
【frameworks/base/media/jni/android_media_MediaScanner.cpp】

1virtual status_t scanFile(const char* path, long long lastModified, 2        long long fileSize, bool isDirectory, bool noMedia) 3{ 4    . . . . . . 5    jstring pathStr; 6    if ((pathStr = mEnv->NewStringUTF(path)) == NULL) { 7        mEnv->ExceptionClear(); 8        return NO_MEMORY; 9    } 10    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, 11            fileSize, isDirectory, noMedia); 12    mEnv->DeleteLocalRef(pathStr); 13    return checkAndClearExceptionFromCallback(mEnv, "scanFile"); 14}

【frameworks/base/media/java/android/media/MediaScanner.java】

1@Override 2public void scanFile(String path, long lastModified, long fileSize, 3        boolean isDirectory, boolean noMedia) { 4    doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 5}

调用到doScanFile()函数。

现在我们再画一张关于扫描目录的简单示意图:

2.3.4.3 doScanFile()和MediaProvider

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

1public Uri doScanFile(String path, String mimeType, long lastModified, 2        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 3    . . . . . . 4        FileEntry entry = beginFile(path, mimeType, lastModified, 5                fileSize, isDirectory, noMedia); 6        . . . . . . 7                if (isaudio || isvideo) { 8                    processFile(path, mimeType, this); 9                } 10                if (isimage) { 11                    processImageFile(path); 12                } 13                result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 14    . . . . . . 15    return result; 16}

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

beginFile()是为了后续和MediaProvider打交道,准备一个FileEntry。FileEntry的定义如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1private static class FileEntry { 2    long mRowId; 3    String mPath; 4    long mLastModified; 5    int mFormat; 6    boolean mLastModifiedChanged; 7 8    FileEntry(long rowId, String path, long lastModified, int format) { 9        mRowId = rowId; 10        mPath = path; 11        mLastModified = lastModified; 12        mFormat = format; 13        mLastModifiedChanged = false; 14    } 15    . . . . . . 16}

FileEntry的几个成员变量,其实体现了查表时的若干列的值。

beginFile()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1public FileEntry beginFile(String path, String mimeType, long lastModified, 2        long fileSize, boolean isDirectory, boolean noMedia) { 3    . . . . . . 4    FileEntry entry = makeEntryFor(path);   // 从MediaProvider中查出该文件或目录对应的入口 5    . . . . . . 6    if (entry == null || wasModified) { 7        if (wasModified) { 8            entry.mLastModified = lastModified; 9        } else { 10            // 如果前面没查到FileEntry,就在这里new一个新的FileEntry 11            entry = new FileEntry(0, path, lastModified, 12                    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0)); 13        } 14        entry.mLastModifiedChanged = true; 15    } 16    . . . . . . 17    return entry; 18}

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

1FileEntry makeEntryFor(String path) { 2    String where; 3    String[] selectionArgs; 4 5    Cursor c = null; 6    try { 7        where = Files.FileColumns.DATA + "=?"; 8        selectionArgs = new String[] { path }; 9        c = mMediaProvider.query(mPackageName, mFilesUriNoNotify,  10                                      FILES_PRESCAN_PROJECTION, 11                                      where, selectionArgs, null, null); 12        if (c.moveToFirst()) { 13            long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 14            int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 15            long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 16            return new FileEntry(rowId, path, lastModified, format); 17        } 18    } catch (RemoteException e) { 19    } finally { 20        if (c != null) { 21            c.close(); 22        } 23    } 24    return null; 25}

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

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

看到了吗,特意要去查一下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(mPackageName, tableUri, values);

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

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

这就是改变MediaProvider中相关信息的最核心句子啦。

endFile()的代码截选如下:
【frameworks/base/media/java/android/media/MediaScanner.java】

1private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications, 2        boolean alarms, boolean music, boolean podcasts) 3        throws RemoteException { 4    . . . . . . 5    ContentValues values = toValues(); 6    String title = values.getAsString(MediaStore.MediaColumns.TITLE); 7    if (title == null || TextUtils.isEmpty(title.trim())) { 8        title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 9        values.put(MediaStore.MediaColumns.TITLE, title); 10    } 11    . . . . . . 12    long rowId = entry.mRowId; 13    if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 14        . . . . . . 15        values.put(Audio.Media.IS_ALARM, alarms); 16        values.put(Audio.Media.IS_MUSIC, music); 17        values.put(Audio.Media.IS_PODCAST, podcasts); 18    } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) { 19        . . . . . . 20    } 21 22    . . . . . . 23    if (rowId == 0) { 24        . . . . . . 25        // 扫描的是新文件,insert记录。如果是目录的话,必须比它所含有的所有文件更早插入记录, 26        // 所以在批量插入时,就需要有更高的优先权。如果是文件的话,而且我们现在就需要其对应 27        // 的rowId,那么应该立即进行插入,此时不过多考虑批量插入。 28        if (inserter == null || needToSetSettings) { 29            if (inserter != null) { 30                inserter.flushAll(); 31            } 32            result = mMediaProvider.insert(mPackageName, tableUri, values); 33        } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 34            inserter.insertwithPriority(tableUri, values); 35        } else { 36            inserter.insert(tableUri, values); 37        } 38 39        if (result != null) { 40            rowId = ContentUris.parseId(result); 41            entry.mRowId = rowId; 42        } 43    } else { 44        . . . . . . 45        mMediaProvider.update(mPackageName, result, values, null, null); 46    } 47    . . . . . . 48    return result; 49}

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

MediaInserter也是向MediaProvider中写入数据,最终大体上会走到其flush()函数,该函数的代码如下:
【frameworks/base/media/java/android/media/MediaInserter.java】

1    private void flush(Uri tableUri, List<ContentValues> list) throws RemoteException { 2        if (!list.isEmpty()) { 3            ContentValues[] valuesArray = new ContentValues[list.size()]; 4            valuesArray = list.toArray(valuesArray); 5            mProvider.bulkInsert(mPackageName, tableUri, valuesArray); 6            list.clear(); 7        } 8    }

3 小节

写了这么多,终于看到MediaScannerService是如何更新MediaProvider的了。当然,里面还有大量的细节,本文就不展开来讲了,要不然相信大家头壳都得炸掉。那么就先写这么多了。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )