导航号,我的单页导航
文章目录

前言

MediaProvider继承自ContentProvider,是Android用于存储图片、音频、视频和文档等多媒体信息,提供给其他需要的应用使用。

今天就对MediaProvider应用代码进行一定学习。参考网上大佬文章,记录一下过程。

这里是用了Android P源码分析

正文

MediaProvider存在路径:

\packages\providers\MediaProvider

扫描后存在的数据目录

/data/data/com.android.providers.media/databases
# 或
/data/user_de/0/com.android.providers.media/databases

一般是在第一个目录,但也存在放在第二个目录,这个具体看平台是否有改动。

MediaProvider存在两个数据库

internal.db #内部数据库
external.db #外部数据库
重点

internal.db是查询指定目录的数据

//[/system/media, /oem/media, /product/media]
directories = new String[] {
        Environment.getRootDirectory() + "/media",
        Environment.getOemDirectory() + "/media",
        Environment.getProductDirectory() + "/media",
};

external.db 是包括sd目录和插入的磁盘目录中所有数据。

进入扫描

在继续跟进时,我们需要知道有几个入口让MediaProvider进入扫描,先看大佬们整理的

MediaProvider源码分析

从上面看,可以知道,进入扫描于两种方式

  1. 通过广播监听

  2. 通过服务绑定

通过这两种方式,我们进入分析MediaProvider。

广播监听

一般广播监听有两种,一种是动态广播,一种是静态广播,各有优点。一般看一个应用会先看AndroidManifest.xml配置。

AndroidManifest.xml

我们关注需要的,比如磁盘挂载等广播,我们会发现MediaScannerReceiver就是我们需要找的广播。

<receiver android:name="MediaScannerReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.LOCALE_CHANGED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_MOUNTED" />
        <data android:scheme="file" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
        <data android:scheme="file" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
        <data android:scheme="file" />
    </intent-filter>
</receiver>

上面监听了好些重要的广播,我们直接进入看MediaScannerReceiver怎么处理的。

MediaScannerReceiver.java
public void onReceive(Context context, Intent intent) {
    final String action = intent.getAction();
    final Uri uri = intent.getData();
    if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
        //开机广播,只扫描内置磁盘,内置的磁盘一定存在
        //INTERNAL_VOLUME = "internal"
        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();
            // 我这平台上 externalStoragePath = "/storage/emulated/0";
            String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
            // 我这平台上 legacyPath = "/sdcard";
            String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();
            try {
                path = new File(path).getCanonicalPath();
            } catch (IOException e) {
                return;
            }
            if (path.startsWith(legacyPath)) {
                //需要用绝对路径,确保统一
                path = externalStoragePath + path.substring(legacyPath.length());
            }
            if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                //根据磁盘挂载进行扫描外置磁盘
                //EXTERNAL_VOLUME = "external"
                scan(context, MediaProvider.EXTERNAL_VOLUME);
            } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                path != null && path.startsWith(externalStoragePath + "/")) {
                //好奇!!!
                //有startsWith条件判断,这里只是针对内置文件扫描?挂载的U盘路径就无法满足!
                //哈哈,对数据库内容,只有外部数据库才会更新
                //[内置sd卡和挂载是磁盘都是外部数据库的]
                scanFile(context, path);
            }
        }
    }
}

上面涉及如下几个方法

  1. scan(Context context, String volume) //扫描内置或外置磁盘

  2. void scanFile(Context context, String path) //扫描指定文件[只能是文件!]

  3. scanTranslatable(Context context) // 设备语言变化了调用

上面是哪个方法有个共同点,都是startService()启动了MediaScannerService

context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
MediaScannerService.java

MediaScannerService是个服务,一般都先看onCreate()和onStartCommand()。

  1. onCreate()

    public void onCreate() {
        //防止扫描过程中CPU睡死过去,扫描中需要
        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
        //略
        //启动子线程,第二个参数Runnable传入的是this,也就是说MediaScannerService实现了run()
        Thread thr = new Thread(null, this, "MediaScannerService");
        thr.start();
    }
  2. onStartCommand()

    public int onStartCommand(Intent intent, int flags, int startId) {
        //等待mServiceHandler初始化
        while (mServiceHandler == null) {
            synchronized (this) {
                try {
                    wait(100);
                } catch (InterruptedException e) {
                }
            }
        }
    	//略
    	//放入消息队列,让ServiceHandler处理。
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent.getExtras();
        mServiceHandler.sendMessage(msg);
        return Service.START_REDELIVER_INTENT;
    }

这里主要是把intent中参数丢入消息队列,让mServiceHandler处理。

  1. run()

    上面启动了子线程,MediaScannerService也实现了run()

    public void run() {
        //将优先级降低到其他后台线程以下以避免干扰其他服务。
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
                Process.THREAD_PRIORITY_LESS_FAVORABLE);
    	//很熟悉吧,启动Looper,这样就可以使用ServiceHandler
        Looper.prepare();
        mServiceLooper = Looper.myLooper();
        mServiceHandler = new ServiceHandler();
        Looper.loop();
    }
  2. ServiceHandler

    private final class ServiceHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Bundle arguments = (Bundle) msg.obj;
            if (arguments == null) {
                return;
            }
            String filePath = arguments.getString("filepath");
            try {
    		    //文件扫描
                if (filePath != null) {
                    IBinder binder = arguments.getIBinder("listener");
                    //如果是从广播来的,binder= null
                    IMediaScannerListener listener = 
                            (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
                    Uri uri = null;
                    try {
                     	//【重点】scanFile
                        uri = scanFile(filePath, arguments.getString("mimetype"));
                    } catch (Exception e) {
                        Log.e(TAG, "Exception scanning file", e);
                    }
                    if (listener != null) {
                        listener.scanCompleted(filePath, uri);
                    }
                } 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;
                        }
                    }
                    if (directories != null) {
                        //【重点】scan
                        scan(directories, volume);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception in handleMessage", e);
            }
    		//扫描结束,自动销毁
            stopSelf(msg.arg1);
        }
    }

    上面涉及scanFile()和scan()方法。主要是的区别是前者扫描文件,后者是扫描磁盘。

  3. scan()

    private void scan(String[] directories, String volumeName) {
        Uri uri = Uri.parse("file://" + directories[0]);
    	//扫描中不让休眠
        mWakeLock.acquire();
        try {
            ContentValues values = new ContentValues();
    		//MEDIA_SCANNER_VOLUME = "volume"
            values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
    		//让MeidaProvider做一些准备工作??(没有具体看,网上大佬这么说的)
    		//scanUri = content://media/none/media_scanner
            Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
    		//发出开始扫描广播
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
            try {
                if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
    				// 打开EXTERNAL数据库[为啥要判断,可以看文章中【重点】部分]
                    openDatabase(volumeName);
                }
    			//创建MediaScanner,调用scanDirectories扫描目标文件夹。
                try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                	//directories可能是[/storage/emulated/0, /storage/udisk0]
                    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));
    		Log.d(TAG, "scan +++++ ACTION_MEDIA_SCANNER_FINISHED +++++  : ");
            mWakeLock.release();
        }
    }

    上面正在扫描工作是在MediaScanner中。

  4. scanFile()

    private Uri scanFile(String path, String mimeType) {
        String volumeName = MediaProvider.EXTERNAL_VOLUME;
        try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
            String canonicalPath = new File(path).getCanonicalPath();
            return scanner.scanSingleFile(canonicalPath, mimeType);
        } catch (Exception e) {
            return null;
        }
    }

    scanFile()指定扫描某个文件,最终的扫描逻辑依旧在MediaScanner中。

MediaScanner后续单独分析,这里不做过多的介绍。从上面看,从广播进入扫描的就分析完了。

服务绑定

除了ACTION_MEDIA_SCANNER_SCAN_FILE广播,Android还可以通过bindService机制得到IMediaScannerService代理接口,再通过该接口的requestScanFile()或scanFile()请求扫描,不过这个只能扫描文件。

先看看MediaScannerService返回的IBinder

@Override
public IBinder onBind(Intent intent) {
    //返回binder代理
    return mBinder;
}

private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() {
	//方法一
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener) {
        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);
    }
};

有两个方法requestScanFile()和scanFile(),最终还是调用了requestScanFile,也就是带上参数并启动服务。

其中IMediaScannerService定义的的目录

frameworks\base\media\java\android\media\IMediaScannerService.aidl

由于一般不涉及这块,懒得继续看。推荐最后的参考文章,此文对这块讲解比较详细。

参考文章

  1. Android MediaScanner:(二)MediaScannerReceiver

  2. MediaProvider流程分析Android 9和Android 10版本

  3. MediaProvider流程分析

  4. Android MediaProvider

  5. MediaScannerService 研究》[推荐看这个]

© 版权声明
导航号,我的单页导航

暂无评论

暂无评论...