MediaStore数据库分析

Posted xiaoqiang_0719

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MediaStore数据库分析相关的知识,希望对你有一定的参考价值。

MediaStore

概述

MediaStore是android系统提供的一个多媒体数据库,专门用于存放多媒体信息,通过ContentResolver可对数据库进行操作。

本篇文章会讲述一个标准的从MediaStore数据库获取图片数据并展示到自己使用RecyclerView实现的相册中的过程,并且会带着以下几个问题来解释为什么要这么做。

  • MediaStore、MediaScannerReceiver、MediaScannerService、MediaScanner、ContentResolver、MediaScannerConnection 是如何实现数据库的增删改查操作的?

  • 为什么网络图片下载到手机之后相册里还是没有数据?

  • 如何做到下载图片之后可以立马在相册中显示?

  • 图片在文件管理中删除之后MediaStore不更新问题?(真实工作中自家Rom出现的问题)

读取MediaStore数据、设置监听、显示数据

查询数据

通过ContentResolver提供的查询接口来获取MediaStore数据(对应下述 扩展说明A)

注册监听

调用ContentResolver的registerContentObserver方法来设置监听(对应下述 扩展说明B)

    fun loadImages() {
        viewModelScope.launch {
            val imageList = queryImages()  //-> A
            _images.postValue(imageList)

            if (contentObserver == null) {  //-> B
                contentObserver = getApplication<Application>().contentResolver.registerObserver(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                ) {
                    loadImages()
                }
            }
        }
    }

代码扩展说明

注释点 **A ** --> queryImages() 方法使用ContentResolver获取图片列表并赋值给imageList,然后通过LiveDate的postValue发送出去,Activity会监听此LiveDate,从而拿到imageList进行ListAdapter的刷新操作。queryImages()具体方法如下:

    private suspend fun queryImages(): List<MediaStoreImage> {
        val images = mutableListOf<MediaStoreImage>()

        withContext(Dispatchers.IO) {

            val projection = arrayOf(
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.DATE_ADDED
            )
            val selection = "${MediaStore.Images.Media.DATE_ADDED} >= ?"

            val selectionArgs = arrayOf(
                dateToTimestamp(day = 22, month = 10, year = 2008).toString()
            )

            val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"

            getApplication<Application>().contentResolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                projection,
                selection,
                selectionArgs,
                sortOrder
            )?.use { cursor ->
                val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
                val dateModifiedColumn =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
                val displayNameColumn =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)

                Log.i(TAG, "Found ${cursor.count} images")
                while (cursor.moveToNext()) {
                    val id = cursor.getLong(idColumn)
                    val dateModified =
                        Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn)))
                    val displayName = cursor.getString(displayNameColumn)
                    val contentUri = ContentUris.withAppendedId(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        id
                    )
                    val image = MediaStoreImage(id, displayName, dateModified, contentUri)
                    images += image
                    Log.v(TAG, "Added image: $image")
                }
            }
        }
        Log.v(TAG, "Found ${images.size} images")
        return images
    }

    /**
     *  日期格式化
     */
    private fun dateToTimestamp(day: Int, month: Int, year: Int): Long =
        SimpleDateFormat("dd.MM.yyyy").let { formatter ->
            TimeUnit.MICROSECONDS.toSeconds(formatter.parse("$day.$month.$year")?.time ?: 0)
        }

//上述代码内容有点多,稍微做一下阐述,有以下几步操作:

//1、withContext(Dispatchers.IO) 协程切换到IO线程执行
//2、创建projection、selection、selectionArgs、sortOrder这几个query需要的参数
//3、执行query操作并返回Cursor对象
//4、通过Cursor对象获取图片的参数并创建MediaStoreImage对象
//5、将MediaStoreImage添加到list并返回


//MediaStoreImage是我们定义的bean对象,用于在RecycleView中数据的显示
data class MediaStoreImage(
    val id: Long,
    val displayName: String,
    val dateAdded: Date,
    val contentUri: Uri
)

注释点 B --> registerObserver是我定义的ContentResolver的一个扩展方法,他实现了对MediaStore.Images.Media.EXTERNAL_CONTENT_URI的监听,当数据变化会重新调用loadImages()方法–>queryImages()方法,获取MediaStore数据 。ContentResolver的扩展方法如下:

private fun ContentResolver.registerObserver(
    uri: Uri,
    observer: (selfChange: Boolean) -> Unit
): ContentObserver {
    val contentObserver = object : ContentObserver(Handler()) {
        override fun onChange(selfChange: Boolean) {
            observer(selfChange)
        }
    }
    registerContentObserver(uri, true, contentObserver)
    return contentObserver
}

上述两个注释点A|B都会调用queryImages()方法来获取图片列表,A是主动触发,而B是MediaStore数据库更新之后被动触发,所以此时的代码我们已经可是保证images可以跟MediaStore下的图片保持一致了。

界面显示

最后是界面显示,我使用的是RecyclerView.Adapter的子类ListAdapter来实现列表显示和更新,并且将点击事件作为参数传入到Adapter(高阶函数方式),这个没什么好说的,不懂的可已看下ListAdapter标准写法

class GalleryAdapter(private val onClick: (MediaStoreImage) -> Unit) :
    ListAdapter<MediaStoreImage, ImageViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val view = layoutInflater.inflate(R.layout.gallery_layout, parent, false)
        return ImageViewHolder(view, onClick)
    }

    override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
        val mediaStoreImage = getItem(position)
        holder.rootView.tag = mediaStoreImage

        Glide.with(holder.imageView)
            .load(mediaStoreImage.contentUri)
            .thumbnail(0.33f)
            .centerCrop()
            .into(holder.imageView)
    }

    companion object {
        val DiffCallback = object : DiffUtil.ItemCallback<MediaStoreImage>() {
            override fun areItemsTheSame(oldItem: MediaStoreImage, newItem: MediaStoreImage) =
                oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: MediaStoreImage, newItem: MediaStoreImage) =
                oldItem == newItem
        }
    }
}

添加点击事件

// 初始化GalleryAdapter,并通过高阶函数方式传入一个deleteImage方法
val galleryAdapter = GalleryAdapter { image ->
            deleteImage(image)
        }


//界面显示的关键方法,监听了viewModel里的images(是个LiveDate),拿到List<MediaStoreImage>更新ListAdapter
viewModel.images.observe(this, Observer<List<MediaStoreImage>> { images ->
            galleryAdapter.submitList(images)
        })


//deleteImage方法会在点击Adapter中任意一项的时候弹出Dialog,会调用deleteImage
// 最终调用contentResolver的delete来删除此项Image,篇幅有限删除请查看最终代码
private fun deleteImage(image: MediaStoreImage) {
        MaterialAlertDialogBuilder(this)
            .setTitle(R.string.delete_dialog_title)
            .setMessage(getString(R.string.delete_dialog_message, image.displayName))
            .setPositiveButton(R.string.delete_dialog_positive) { _: DialogInterface, _: Int ->
                viewModel.deleteImage(image)
            }
            .setNegativeButton(R.string.delete_dialog_negative) { dialog: DialogInterface, _: Int ->
                dialog.dismiss()
            }
            .show()
    }

最终代码

https://github.com/WuMaoQiang/nobo_mediastore

问题处理

为什么网络图片下载到手机之后相册里还是没有数据?

MediaStore的刷新不是实时的,比如开机,U盘挂载等场景会触发刷新,当我们下载一张网路图片,虽然图片下载成功了,在文件管理中可以找到这个图片,但是相册中却看不到,这是MediaStore设计如此。所以如果我们想让相册或者任何关心MediaStore的应用能立马看到这个图片那就要主动触发刷新MediaStore。

如何做到下载图片之后可以立马在相册中显示?

有三种方式可以做到:

1、使用MediaStore.Image.Media.inserImage()方法,直接通过ContentResolver操作数据库

在这里插入图片描述

2、发送ACTION_MEDIA_SCANNER_SCAN_FILE广播。查看系统类 广播类MediaScannerReceiver会对此Action做出响应,调用scanFile方法

在这里插入图片描述

而ScanFile方法启动了一个Service进行Scan操作

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

MediaScannerService创建MediaScanner执行scanSingleFile进行全盘扫描操作

在这里插入图片描述

3、使用MediaScannerConnection类进行全盘扫描,通过绑定 MediaScannerService 进行Scan操作,MediaScannerConnection会调用scanFile方法通过 connection.connect();来进行与MediaScannerService 的绑定操作

233    public static void scanFile(Context context, String[] paths, String[] mimeTypes,
234            OnScanCompletedListener callback) {
235        ClientProxy client = new ClientProxy(paths, mimeTypes, callback);
236        MediaScannerConnection connection = new MediaScannerConnection(context, client);
237        client.mConnection = connection;
238        connection.connect();
239    }
240
    
112    public void connect() {
113        synchronized (this) {
114            if (!mConnected) {
115                Intent intent = new Intent(IMediaScannerService.class.getName());
116                intent.setComponent(
117                        new ComponentName("com.android.providers.media",
118                                "com.android.providers.media.MediaScannerService"));
119                mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
120                mConnected = true;
121            }
122        }
123    }

绑定MediaScannerService 之后也会调用它的 scanFile方法,之后在调用MediaScanner进行扫描操作

图片在文件管理中删除之后MediaStore不更新问题?

此问题是我再实际开发中碰到的问题,文件管理开发工程师在删除相应图片之后未通过上述手段及时通知相册或未触发重新扫描操作,导致图片确实已经删除了,但是我的相册监听的是MediaStore数据库并没有更新。

后面文件管理工程师在删除图片之后,采用了发送广播的形式来触发MediaStore重新扫描,理论上是可以实现刷新MediaStore数据库的,但是事实上没有奏效,经过排查发现,广播和形式或者MediaScannerService 的形式都无法触发MediaStore数据库的重新扫描,因为源码中如果文件已经被删除了,那么MediaScanner就return了

在这里插入图片描述

所以我们只能采用contentResolver操作MediaStore数据库的方式来删除数据

                getApplication<Application>().contentResolver.delete(
                    image.contentUri,
                    "${MediaStore.Images.Media._ID} = ?",
                    arrayOf(image.id.toString())
                )

以上是关于MediaStore数据库分析的主要内容,如果未能解决你的问题,请参考以下文章

MediaStore数据库分析

MediaStore数据库分析

从片段中捕获图像

Android - 片段的 onActivityResult() 中的 NPE

片段中的 startActivityForResult

MediaStore.EXTRA_OUTPUT 呈现数据为空,其他保存照片的方式?