美文网首页ContentProvider安卓进阶
Media Data之多媒体数据库(二)MediaProvide

Media Data之多媒体数据库(二)MediaProvide

作者: Lemon_Home | 来源:发表于2017-05-31 16:04 被阅读313次

    MediaProvider使用 SQLite 数据库存储图片、视频、音频等多媒体文件的信息,供视频播放器、音乐播放器、图库使用。提供了基本的增删改查等相关方法。路径如下:
    /packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
      其中包含以下内部类:
        DatabaseHelper——对于一个特殊数据库的包装类,用来管理数据的创建和版本更新,继承SQLiteOpenHelper
        GetTableAndWhereOutParameter——静态类,获取相关的参数
        ScannerClient——静态类,继承MediaScannerConnectionClient
        ThumbData——方便对变量的操作
      下面依据对数据库的操作进行分析。

    1. 创建

    首先在MediaProvider的onCreate方法中分别对内部存储和外部存储进行链接数据库:

    //绑定内部存储数据库
    attachVolume(INTERNAL_VOLUME);
    ...
    if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        //绑定外部存储数据库
        attachVolume(EXTERNAL_VOLUME);
    }
    

    为内部存储和外部存储进行创建数据库,如果此存储卷已经链接上了,那么什么也不做,否则就查询存储卷的id并且建立对应的数据库。接下来分析attachVolume方法:

    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,传参,之后调用DatabaseHelper的方法
                helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
                        false, mObjectRemovedCallback);
            } else if (EXTERNAL_VOLUME.equals(volume)) {
                //如果是外部存储的操作
                final VolumeInfo vol = mStorageManager.getPrimaryPhysicalVolume();
                if (vol != null) {
                    final StorageVolume actualVolume = mStorageManager.getPrimaryVolume();
                    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)) {
                        //已经挂载但是sd卡是只读状态
                        } else {
                            //还没有挂载
                        }
                    }
    
                    // 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
                    ... ...
                    //根据之前转换的数据库名,创建数据库
                    helper = new DatabaseHelper(context, dbFile.getName(), false,
                            false, mObjectRemovedCallback);
                }
            } else {
                throw new IllegalArgumentException("There is no volume named " + volume);
            }
            //标识已经创建过了数据库
            mDatabases.put(volume, helper);
    
            if (!helper.mInternal) {
                // clean up stray album art files: delete every file not in the database
                File[] files = new File(mExternalStoragePaths[0],
                               ALBUM_THUMB_FOLDER).listFiles();
                HashSet<String> fileSet = new HashSet();
                for (int i = 0; files != null && i < files.length; i++) {
                    fileSet.add(files[i].getPath());
                }
                Cursor cursor = query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
                        new String[] { MediaStore.Audio.Albums.ALBUM_ART }, null, null, null);
                try {
                    while (cursor != null && cursor.moveToNext()) {
                        fileSet.remove(cursor.getString(0));
                    }
                } finally {
                    IoUtils.closeQuietly(cursor);
                }
                Iterator<String> iterator = fileSet.iterator();
                while (iterator.hasNext()) {
                    String filename = iterator.next();
                    if (LOCAL_LOGV) Log.v(TAG, "deleting obsolete album art " + filename);
                    new File(filename).delete();
                }
            }
        }
        if (EXTERNAL_VOLUME.equals(volume)) {
            //给外部存储创建默认的文件夹
            ensureDefaultFolders(helper, helper.getWritableDatabase());
        }
        return Uri.parse("content://media/" + volume);
    }
    

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

    @Override
    public void onCreate(final SQLiteDatabase db) {
        //在此方法中对63版本以下的都会新建数据库
        updateDatabase(mContext, db, mInternal, 0, getDatabaseVersion(mContext));
    }
    @Override
    public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
        //对数据库进行更新
        mUpgradeAttempted = true;
        updateDatabase(mContext, db, mInternal, oldV, newV);
    }
    

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

    private static void updateDatabase(Context context, SQLiteDatabase db, boolean internal,
            int fromVersion, int toVersion) {
        // sanity checks
        int dbversion = getDatabaseVersion(context);
        //对数据库的版本进行判断
        ... ...
        long startTime = SystemClock.currentTimeMicro();
        //对传入的数据库版本进行判断,如果小于63,或者在84到89,92到94之间的,
        //都会去创建数据库
        if (fromVersion < 63 || (fromVersion >= 84 && fromVersion <= 89) ||
                (fromVersion >= 92 && fromVersion <= 94)) {
        //下面就是执行具体的sqlite CRATE语句,创建对应的表
        ... ...
        }
        //下面也是对版本判断之后进行相应操作
        ... ...
        //检查audio_meta的_data值是否是不同的,如果不同就删除audio_meta,
        //在扫描的时候从新创建
        sanityCheck(db, fromVersion);
        long elapsedSeconds = (SystemClock.currentTimeMicro() - startTime) / 1000000;
    }
    

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

    2 更新

    @Override
    public int update(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);
        }
        // special case renaming directories via MTP.
        // in this case we must update all paths in the database with
        // the directory name as a prefix
        ... ...
        //根据匹配的uri进行相应的操作
        switch (match) {
            case AUDIO_MEDIA_ID:
            //更新音乐人和专辑字段。首先从缓存中判断是否有值,如果有直接用缓存中的
            //数据,如果没有再从数据库中查询是否有对应的信息,如果有则更新,
            //如果没有插入这条数据.接下来的操作是增加更新次数,并更新流派
            ... ...
            case VIDEO_MEDIA_ID:
            //更新视频,并且发出生成略缩图请求
            ... ...
            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
            //更新播放列表数据
            ... ...
        }
        ... ...
    }
    

    至此,更新操作已完成。

    3 插入

    关于插入,有两个方法插入,一个是大量的插入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);
        }
        //如果不满足上述的条件,则开启事务进行插入其他的数据
        db.beginTransaction();
        ArrayList<Long> notifyRowIds = new ArrayList<Long>();
        int numInserted = 0;
        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();
        }
    
        // Notify MTP (outside of successful transaction)
        if (uri != null) {
            if (uri.toString().startsWith("content://media/external/")) {
                notifyMtp(notifyRowIds);
            }
        }
        //通知更新
        getContext().getContentResolver().notifyChange(uri, null);
        return numInserted;
    }
    
    @Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        int match = URI_MATCHER.match(uri);
        ArrayList<Long> notifyRowIds = new ArrayList<Long>();
        //只是调用insertInternal进行插入
        Uri newUri = insertInternal(uri, match, initialValues, notifyRowIds);
        if (uri != null) {
            if (uri.toString().startsWith("content://media/external/")) {
                notifyMtp(notifyRowIds);
            }
        }
        // do not signal notification for MTP objects.
        // we will signal instead after file transfer is successful.
        if (newUri != null && match != MTP_OBJECTS) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return newUri;
    }
    

    insertInternal方法比较简单,但是类别较多,暂时不做分析。

    4 删除

    @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()
        //因为如果匹配的uri是扫描过程中的,此时uri直接通过getDatabaseForUri获取不到
        //数据库,需要对uri进行重新拼装
        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);
                //删除掉在数据库的log表记录
                logToDb(database.getWritableDatabase(), msg);
            }
            mMediaScannerVolume = null;
            //因为只涉及到1行,所以返回值是1
            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);
            //初始化DatabaseHelper和SQLiteDatabase
            ... ...
            synchronized (sGetTableAndWhereParam) {
                //拼装字段
                getTableAndWhere(uri, match, userWhere, sGetTableAndWhereParam);
                if (sGetTableAndWhereParam.table.equals("files")) {
                    String deleteparam = 
                           uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
                    if (deleteparam == null || ! deleteparam.equals("false")) {
                        database.mNumQueries++;
                        Cursor c = db.query(sGetTableAndWhereParam.table,
                                sMediaTypeDataId,
                                sGetTableAndWhereParam.where, whereArgs, null, null, null);
                        String [] idvalue = new String[] { "" };
                        String [] playlistvalues = new String[] { "", "" };
                        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, null, null);
                                    try {
                                        while (cc.moveToNext()) {
                                            deleteIfAllowed(uri, cc.getString(0));
                                        }
                                        database.mNumDeletes++;
                                        //删除数据库中的信息
                                        db.delete("thumbnails", "image_id=?", idvalue);
                                    } finally {
                                        IoUtils.closeQuietly(cc);
                                    }
                                } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) {
                                    //如果是视频文件,直接删除源文件
                                    deleteIfAllowed(uri, data);
                                    MediaDocumentsProvider.onMediaStoreDelete(
                                        getContext(),volumeName, 
                                        FileColumns.MEDIA_TYPE_VIDEO, 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);
                        }
                    }
                }
                //对其他的匹配类型进行删除
                switch (match) {
                   //删除MTP,流派信息,视频文件的略缩图
                   ... ...
                }
                // 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 查询

    public Cursor query(Uri uri, String[] projectionIn, String selection,
            String[] selectionArgs, String sort) {
        uri = safeUncanonicalize(uri);
        int table = URI_MATCHER.match(uri);
        List<String> prependArgs = new ArrayList<String>();
        // 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;
        }
    

    至此,关于MediaProvider的增删改查,创建数据库等操作的分析已经完成。

    相关文章

      网友评论

        本文标题:Media Data之多媒体数据库(二)MediaProvide

        本文链接:https://www.haomeiwen.com/subject/nxrjfxtx.html