魅族手机文件删除-通知栏警告流程分析(下)

魅族手机文件删除后通知栏警告流程分析上

先将魅族手机中的关键jar包pull出来贴这里

MediaProvider.apk
链接: https://pan.baidu.com/s/1HT4FETwg9KS_8ewctSNEiA 提取码: hwme

/system/framework/framework.jar
链接: https://pan.baidu.com/s/16RgA2XGlunzSxjjr8NpX7Q 提取码: meek

/system/framework/services.jar
链接: https://pan.baidu.com/s/12jytAxe2Ej6BShrQgZ0rAA 提取码: d8b9

将MediaProvider.smali代码逆向还原

只需要关注关键函数deleteInternal


    public final Pattern PATTERN_RELATIVE_PATH = Pattern.compile("(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)(Android/sandbox/([^/]+)/)?");;

    public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
            "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/?.*)?");


    //FileUtils.getContentUriForPath("/storage/emulated/0/libs/3.txt")
    //content://media/external_primary/file
    private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
            throws MediaProvider.FallbackException {
        extras = (extras != null) ? extras : new Bundle();

        // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
        extras.remove(INCLUDED_DEFAULT_DIRECTORIES);

        uri = safeUncanonicalize(uri);
        final boolean allowHidden = isCallingPackageAllowedHidden();
        final int match = matchUri(uri, allowHidden);

        switch(match) {
            case AUDIO_MEDIA_ID:
            case AUDIO_PLAYLISTS_ID:
            case VIDEO_MEDIA_ID:
            case IMAGES_MEDIA_ID:
            case DOWNLOADS_ID:
            case FILES_ID: {
                if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()).
                        removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) {
                    // Apps sometimes delete the file via filePath and then try to delete the db row
                    // using MediaProvider#delete. Since we would have already deleted the db row
                    // during the filePath operation, the latter will result in a security
                    // exception. Apps which don't expect an exception will break here. Since we
                    // have already deleted the db row, silently return zero as deleted count.
                    return 0;
                }
            }
            break;
            default:
                // For other match types, given uri will not correspond to a valid file.
                break;
        }

        final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
        final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);

        int count = 0;

        final String volumeName = getVolumeName(uri);

        //魅族手机-------begin--------
        if (!volumeName.equals("internal")) {
            StringBuilder builder = new StringBuilder();
            builder.append("delete: uri=");
            builder.append(volumeName);
            builder.append(", userWhere=");
            builder.append(userWhere);
            builder.append(", userWhereArgs:");
            builder.append(Arrays.toString(userWhereArgs));
            saveLogToFile(context, builder.toString());
        }
        //魅族手机-------end--------
        final int targetSdkVersion = getCallingPackageTargetSdkVersion();

        // handle MEDIA_SCANNER before calling getDatabaseForUri()
        if (match == MEDIA_SCANNER) {
            if (mMediaScannerVolume == null) {
                return 0;
            }

            final DatabaseHelper helper = getDatabaseForUri(
                    MediaStore.Files.getContentUri(mMediaScannerVolume));

            helper.mScanStopTime = SystemClock.elapsedRealtime();

            mMediaScannerVolume = null;
            return 1;
        }

        if (match == VOLUMES_ID) {
            detachVolume(uri);
            count = 1;
        }

        switch (match) {
            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
                extras.putString(QUERY_ARG_SQL_SELECTION,
                        BaseColumns._ID + "=" + uri.getPathSegments().get(5));
                // fall-through
            case AUDIO_PLAYLISTS_ID_MEMBERS: {
                final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
                final Uri playlistUri = ContentUris.withAppendedId(
                        MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);

                // Playlist contents are always persisted directly into playlist
                // files on disk to ensure that we can reliably migrate between
                // devices and recover from database corruption
                return removePlaylistMembers(playlistUri, extras);
            }
        }

        //魅族手机-----begin---------
        ArrayList delArrays = new ArrayList();
        boolean needDelProtection = isNeedDeletionProtection();
        //一般情况三方app均返回true
        //魅族手机-------end----------


        final DatabaseHelper helper = getDatabaseForUri(uri);
        final com.android.providers.media.util.SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);

        {
            // Give callers interacting with a specific media item a chance to
            // escalate access if they don't already have it
            switch (match) {
                case AUDIO_MEDIA_ID:
                case VIDEO_MEDIA_ID:
                case IMAGES_MEDIA_ID:
                    enforceCallingPermission(uri, extras, true);
            }

            /**
             *
             *  5676     const-string v16, "media_type"
             *  5677
             *  5678     const-string v17, "_data"
             *  5679
             *  5680     const-string v18, "_id"
             *  5681
             *  5682     const-string v19, "is_download"
             *  5683
             *  5684     const-string v20, "mime_type"
             *  5685
             *  5686     const-string v21, "is_trashed"
             *  5687
             *  5688     const-string v22, "_size"
             */

            final String[] projection = new String[] {
                    MediaStore.Files.FileColumns.MEDIA_TYPE,
                    MediaStore.Files.FileColumns.DATA,
                    MediaStore.Files.FileColumns._ID,
                    MediaStore.Files.FileColumns.IS_DOWNLOAD,
                    MediaStore.Files.FileColumns.MIME_TYPE,
                    "is_trashed",/*魅族手机*/
                    "_size",/*魅族手机*/
            };
            final boolean isFilesTable = qb.getTables().equals("files");
            final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
            if (isFilesTable) {
                String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
                if (deleteparam == null || ! deleteparam.equals("false")) {
                    Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
                            null, null, null, null, null);
                    try {
                        while (c.moveToNext()) {
                            final int mediaType = c.getInt(0);
                            final String data = c.getString(1);
                            final long id = c.getLong(2);
                            final int isDownload = c.getInt(3);
                            final String mimeType = c.getString(4);

                            //魅族手机--------begin-------------
                            final int isTrashed = c.getInt(5);
                            final long size = c.getInt(6);
                            boolean checkWriteRead = checkPermissonForMediaStore(data, false);

                            //一般应用申请了sdcard读写权限,legacy模式,checkWriteRead就会为true,代表data路径可读写
                            if (!checkWriteRead) {
                                StringBuilder builder = new StringBuilder();
                                builder.append("is not allowed to delete :");
                                builder.append(data);
                                Log.w(TAG, builder.toString());
                                continue;
                            }
                            if (needDelProtection && isTrashed == 1) {
                                int cc = count + 1;
                                StringBuilder builder = new StringBuilder();
                                builder.append("@_@ deleteInternal file has trashed already count = ");
                                builder.append(cc);
                                Log.d(TAG, builder.toString());
                            }

                            final String callingPkg = getCallingPackageOrSelf();
                            File ff = new File(data);
                            boolean isDir = ff.isDirectory();
                            final String topDir = getTopLevelDir(data, isDir);
                            boolean isSelfDir = isAppSelfFile(callingPkg, topDir, data);
                            if (!isSelfDir) {
                                int i = handleTrashOneItem(data, id, mediaType, mimeType, size, delArrays);
                                count += i;
                            }
                            //魅族手机--------end--------------

                            // Forget that caller is owner of this item
                            mCallingIdentity.get().setOwned(id, false);

                            deleteIfAllowed(uri, extras, data);
                            count += qb.delete(helper, BaseColumns._ID + "=" + id, null);

                            // Only need to inform DownloadProvider about the downloads deleted on
                            // external volume.
                            if (isDownload == 1) {
                                deletedDownloadIds.put(id, mimeType);
                            }

                            // Update any playlists that reference this item
                            if ((mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO)
                                    && helper.isExternal()) {
                                helper.runWithTransaction((db) -> {
                                    try (Cursor cc = db.query("audio_playlists_map",
                                            new String[] { "playlist_id" }, "audio_id=" + id,
                                            null, "playlist_id", null, null)) {
                                        while (cc.moveToNext()) {
                                            final Uri playlistUri = ContentUris.withAppendedId(
                                                    MediaStore.Audio.Playlists.getContentUri(volumeName),
                                                    cc.getLong(0));
                                            resolvePlaylistMembers(playlistUri);
                                        }
                                    }
                                    return null;
                                });
                            }
                        }
                    } finally {
                        com.android.providers.media.util.FileUtils.closeQuietly(c);
                    }
                    // Do not allow deletion if the file/object is referenced as parent
                    // by some other entries. It could cause database corruption.
                    appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE);
                }
            }

            switch (match) {
                case AUDIO_GENRES_ID_MEMBERS:
                    throw new MediaProvider.FallbackException("Genres are read-only", Build.VERSION_CODES.R);

                case IMAGES_THUMBNAILS_ID:
                case IMAGES_THUMBNAILS:
                case VIDEO_THUMBNAILS_ID:
                case VIDEO_THUMBNAILS:
                    // Delete the referenced files first.
                    Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
                            null, null, null, null);
                    if (c != null) {
                        try {
                            while (c.moveToNext()) {
                                deleteIfAllowed(uri, extras, c.getString(0));
                            }
                        } finally {
                            com.android.providers.media.util.FileUtils.closeQuietly(c);
                        }
                    }
                    count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
                    break;

                default:
                    count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
                    break;
            }

            if (deletedDownloadIds.size() > 0) {
                // Do this on a background thread, since we don't want to make binder
                // calls as part of a FUSE call.
                helper.postBackground(() -> {
                    getContext().getSystemService(DownloadManager.class)
                            .onMediaStoreDownloadsDeleted(deletedDownloadIds);
                });
            }

            if (isFilesTable && !isCallingPackageSelf()) {
                com.android.providers.media.util.Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
                        getCallingPackageOrSelf(), count);

                //魅族手机----begin
                if (delArrays.size() > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    final String callingPkg = getCallingPackageOrSelf();
                    ArrayList<ContentValues> pkgDelArrays = sMapDeleteList.get(callingPkg);
                    if (pkgDelArrays == null) {
                        ArrayList<ContentValues> clonedArr = (ArrayList<ContentValues>) delArrays.clone();
                        sMapDeleteList.put(callingPkg, clonedArr);
                    } else {
                        ArrayList<ContentValues> arrays = pkgDelArrays;
                        //删除重复数据,更新sMapDeleteList
                    }

                    Message msg = mHandler.obtainMessage();
                    msg.what = 10;
                    msg.obj = callingPkg;
                    mHandler.sendMessageDelayed(msg, 100);
                }
                //魅族手机----end
            }
        }

        return count;
    }


    private static int handleTrashOneItem(String path, long id, int mediaType, String mimeType, long size, ArrayList<ContentValues> paramArrayList) {
        Uri uri = ContentUris.withAppendedId(FileUtils.getContentUriForPath(path), id);
        ContentValues contentValues1 = new ContentValues();
        contentValues1.put("is_trashed", Integer.valueOf(1));
        ContentValues contentValues2 = new ContentValues();
        contentValues2.put("date_deleted", Long.valueOf(System.currentTimeMillis() / 1000L));
        int i = update(uri, contentValues1, null, null);
        if (i > 0) {
            contentValues2.put("_id", Long.valueOf(id));
            contentValues2.put("_data", path);
            contentValues2.put("_display_name", FileUtils.extractDisplayName(path));
            contentValues2.put("media_type", Integer.valueOf(mediaType));
            contentValues2.put("mime_type", mimeType);
            contentValues2.put("_size", Long.valueOf(size));
            paramArrayList.add(contentValues2);
        }
        return i;
    }

    private static boolean isAppSelfFile(String paramString1, String paramString2, String paramString3) {
        StringBuilder stringBuilder2 = new StringBuilder();
        stringBuilder2.append("@_@ isAppSelfFile pkg: ");
        stringBuilder2.append(paramString1);
        stringBuilder2.append(", topName: ");
        stringBuilder2.append(paramString2);
        stringBuilder2.append(", path: ");
        stringBuilder2.append(paramString3);
        Log.d("MediaProvider", stringBuilder2.toString());
        boolean bool = false;
        try {
            Class<?> clazz = Class.forName("meizu.security.FlymePermissionManager");
            if (checkPkgForExclusiveDirectoryMethod == null)
                checkPkgForExclusiveDirectoryMethod = clazz.getMethod("checkPkgForExclusiveDirectory", new Class[] { String.class, String.class, String.class });
            int i = ((Integer)checkPkgForExclusiveDirectoryMethod.invoke((Object)null, new Object[] { paramString1, paramString2, paramString3 })).intValue();
            if (i == 0)
                bool = true;
        } catch (Exception exception) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("MZSTORAGE-isAppSelfFile ");
            stringBuilder.append(exception.getMessage());
            Log.d("MediaProvider", stringBuilder.toString());
        }
        StringBuilder stringBuilder1 = new StringBuilder();
        stringBuilder1.append("@_@ isAppSelfFile appSelf = ");
        stringBuilder1.append(bool);
        Log.d("MediaProvider", stringBuilder1.toString());
        return bool;
    }
    public static @Nullable String extractPathOwnerPackageName(@Nullable String path) {
        if (path == null) return null;
        final Matcher m = PATTERN_OWNED_PATH.matcher(path);
        if (m.matches()) {
            return m.group(1);
        } else {
            return null;
        }
    }

    private boolean checkPermissonForPath(String filePath, int paramInt1, int paramInt2, boolean paramBoolean) {
        if (isCallingPackageManager() || FileUtils.extractPathOwnerPackageName(filePath) != null || paramInt1 < 10000 || isCtsRunning())
            return true;
        boolean bool = checkPermissonForTopDir(getTopLevelDir(filePath, paramBoolean), paramInt1, paramInt2, filePath);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("check:");
        stringBuilder.append(bool);
        stringBuilder.append(", TopDir:");
        stringBuilder.append(getTopLevelDir(filePath, paramBoolean));
        stringBuilder.append(", uid:");
        stringBuilder.append(paramInt1);
        stringBuilder.append(", path");
        stringBuilder.append(filePath);
        stringBuilder.append(", pathIsDir:");
        stringBuilder.append(paramBoolean);
        Log.d("MediaProvider", stringBuilder.toString());
        return bool;
    }

    private String getTopLevelDir(String path, boolean isDir) {
        boolean bool;
        String str = null;
        if (path == null)
            return null;
        if (path.equals("/storage/emulated/0") || path.equals("/storage/emulated/999"))
            return "/";
        String[] arrayOfString = FileUtils.sanitizePath(FileUtils.extractRelativePath(path));
        if (isDir && arrayOfString.length == 1 && TextUtils.isEmpty(arrayOfString[0])) {
            bool = true;
        } else {
            bool = false;
        }
        if (bool)
            return FileUtils.extractDisplayName(path);
        path = str;
        if (arrayOfString.length >= 1)
            path = arrayOfString[0];
        return path;
    }


    private boolean checkPermissonForMediaStore(String path, boolean paramBoolean) {
        return checkPermissonForPath(path, Binder.getCallingUid(), Binder.getCallingPid(), paramBoolean);
    }

    private boolean isNeedDeletionProtection() {
        String str = getCallingPackageOrSelf();
        return (!isCtsRunning() && !"com.android.providers.media.module".equals(str)
                && !"com.android.providers.media".equals(str))
                ? (isSystemApp(str) ^ true) : false;
    }

    private static void saveLogToFile(Context paramContext, String paramString) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(paramString);
        int i = Binder.getCallingPid();
        String[] arrayOfString = getPackageNames(paramContext, i);//其实就是获取删除文件的app包名
        stringBuilder.append("\ncaller pid = ");
        stringBuilder.append(i);
        stringBuilder.append("\ncaller packageList:");
        stringBuilder.append(Arrays.toString((Object[])arrayOfString));
        stringBuilder.append('\n');
    }

    private static Map<String, ArrayList<ContentValues>> sMapDeleteList =
            new HashMap<String, ArrayList<ContentValues>>();

    private static Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message message) {
            if (message != null) {
                StringBuilder stringBuilder = new StringBuilder();
                stringBuilder.append("handleMessage what = ");
                stringBuilder.append(message.what);
                Log.d("MediaProvider", stringBuilder.toString());
                int i = message.what;
                if (i != 0) {
                    if (i == 10) {
                        MediaProvider mediaProvider = MediaProvider.this;
                        mediaProvider.sendDeleteBroadcasts(mediaProvider.getContext(), (String)message.obj);
                    }
                }
//                else {
//                    try {
//                        if (MediaApplication.getInstance() != null) {
//                            HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
//                            this();
//                            hashMap.put("packageName", "com.android.providers.media.module");
//                            hashMap.put("className", "MediaProvider");
//                            UsageStatsHelper.onEvent("media_provider_called", null, (Map)hashMap);
//                            Log.i("MediaProvider", "Register report scan ...");
//                        } else {
//                            Log.w("MediaProvider", "MediaApplication is null");
//                        }
//                    } catch (Exception exception) {
//                        exception.printStackTrace();
//                    }
//                }
            }
            super.handleMessage(message);
        }
    };
}

deleteInternal拆分讲解

private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
            throws MediaProvider.FallbackException {
        .........
        .........
        
        //魅族手机-----begin---------
        ArrayList delArrays = new ArrayList();
        boolean needDelProtection = isNeedDeletionProtection();
        //一般情况三方app均返回true
        //魅族手机-------end----------        
            
isNeedDeletionProtection函数对于第三方app一般返回true
delArrays非常关键,魅族手机会将删除的文件放到这个list中,后面会分析到
private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
            throws MediaProvider.FallbackException {
        .......
        .......
        
        ......
        final int match = matchUri(uri, allowHidden);
        if (match == MEDIA_SCANNER) {
            if (mMediaScannerVolume == null) {
                return 0;
            }

            final DatabaseHelper helper = getDatabaseForUri(
                    MediaStore.Files.getContentUri(mMediaScannerVolume));

            helper.mScanStopTime = SystemClock.elapsedRealtime();

            mMediaScannerVolume = null;
            return 1;
        }

        if (match == VOLUMES_ID) {
            detachVolume(uri);
            count = 1;
        }

matchUri函数具体不讲,我们下面会贴出来MediaProvider–>UriMatcher

该图中贴出来的代码主要是return返回很关键,因为如果是MEDIA_SCANNER文件会返回,根本不会调用发送通知栏消息的代码。

MediaProvider–>LocalUriMatcher
        public LocalUriMatcher(String auth) {
            mPublic.addURI(auth, "*/images/media", IMAGES_MEDIA);
            mPublic.addURI(auth, "*/images/media/#", IMAGES_MEDIA_ID);
            mPublic.addURI(auth, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL);
            mPublic.addURI(auth, "*/images/thumbnails", IMAGES_THUMBNAILS);
            mPublic.addURI(auth, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);

            mPublic.addURI(auth, "*/audio/media", AUDIO_MEDIA);
            mPublic.addURI(auth, "*/audio/media/#", AUDIO_MEDIA_ID);
            mPublic.addURI(auth, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
            mPublic.addURI(auth, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
            mPublic.addURI(auth, "*/audio/genres", AUDIO_GENRES);
            mPublic.addURI(auth, "*/audio/genres/#", AUDIO_GENRES_ID);
            mPublic.addURI(auth, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
            // TODO: not actually defined in API, but CTS tested
            mPublic.addURI(auth, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS);
            mPublic.addURI(auth, "*/audio/playlists", AUDIO_PLAYLISTS);
            mPublic.addURI(auth, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
            mPublic.addURI(auth, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
            mPublic.addURI(auth, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
            mPublic.addURI(auth, "*/audio/artists", AUDIO_ARTISTS);
            mPublic.addURI(auth, "*/audio/artists/#", AUDIO_ARTISTS_ID);
            mPublic.addURI(auth, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
            mPublic.addURI(auth, "*/audio/albums", AUDIO_ALBUMS);
            mPublic.addURI(auth, "*/audio/albums/#", AUDIO_ALBUMS_ID);
            // TODO: not actually defined in API, but CTS tested
            mPublic.addURI(auth, "*/audio/albumart", AUDIO_ALBUMART);
            // TODO: not actually defined in API, but CTS tested
            mPublic.addURI(auth, "*/audio/albumart/#", AUDIO_ALBUMART_ID);
            // TODO: not actually defined in API, but CTS tested
            mPublic.addURI(auth, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID);

            mPublic.addURI(auth, "*/video/media", VIDEO_MEDIA);
            mPublic.addURI(auth, "*/video/media/#", VIDEO_MEDIA_ID);
            mPublic.addURI(auth, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL);
            mPublic.addURI(auth, "*/video/thumbnails", VIDEO_THUMBNAILS);
            mPublic.addURI(auth, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID);

            mPublic.addURI(auth, "*/media_scanner", MEDIA_SCANNER);

            // NOTE: technically hidden, since Uri is never exposed
            mPublic.addURI(auth, "*/fs_id", FS_ID);
            // NOTE: technically hidden, since Uri is never exposed
            mPublic.addURI(auth, "*/version", VERSION);

            mHidden.addURI(auth, "*", VOLUMES_ID);
            mHidden.addURI(auth, null, VOLUMES);

            //一般删除文件操作都会匹配这个
            mPublic.addURI(auth, "*/file", FILES);
            mPublic.addURI(auth, "*/file/#", FILES_ID);

            mPublic.addURI(auth, "*/downloads", DOWNLOADS);
            mPublic.addURI(auth, "*/downloads/#", DOWNLOADS_ID);
        }

mPublic.addURI(auth, “*/file”, FILES);

一般删除文件操作都会匹配到这个,比如/storage/emulated/0/libs/3.txt. 其uri为content://media/external_primary/file

继续分析deleteInternal函数

private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
            throws MediaProvider.FallbackException {
        .......
        .......
        switch (match) {
            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
                extras.putString(QUERY_ARG_SQL_SELECTION,
                        BaseColumns._ID + "=" + uri.getPathSegments().get(5));
                // fall-through
            case AUDIO_PLAYLISTS_ID_MEMBERS: {
                final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
                final Uri playlistUri = ContentUris.withAppendedId(
                        MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);

                // Playlist contents are always persisted directly into playlist
                // files on disk to ensure that we can reliably migrate between
                // devices and recover from database corruption
                return removePlaylistMembers(playlistUri, extras);
            }
        }

匹配到AUDIO_PLAYLISTS_ID_MEMBERS的文件,也会直接返回,不会发送通知栏消息

private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
            throws MediaProvider.FallbackException {
        .......
        .......
				final DatabaseHelper helper = getDatabaseForUri(uri);
        final com.android.providers.media.util.SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
  
              final String[] projection = new String[] {
                    MediaStore.Files.FileColumns.MEDIA_TYPE,
                    MediaStore.Files.FileColumns.DATA,
                    MediaStore.Files.FileColumns._ID,
                    MediaStore.Files.FileColumns.IS_DOWNLOAD,
                    MediaStore.Files.FileColumns.MIME_TYPE,
                    "is_trashed",/*魅族手机*/
                    "_size",/*魅族手机*/
            };
            final boolean isFilesTable = qb.getTables().equals("files");
            final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
            if (isFilesTable) {
                String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
                if (deleteparam == null || ! deleteparam.equals("false")) {
                    Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
                            null, null, null, null, null);
                    try {
                        while (c.moveToNext()) {
                            final int mediaType = c.getInt(0);
                            final String data = c.getString(1);
                            final long id = c.getLong(2);
                            final int isDownload = c.getInt(3);
                            final String mimeType = c.getString(4);

                            //魅族手机--------begin-------------
                            final int isTrashed = c.getInt(5);
                            final long size = c.getInt(6);
                            boolean checkWriteRead = checkPermissonForMediaStore(data, false);

                            //一般应用申请了sdcard读写权限,legacy模式,checkWriteRead就会为true,代表data路径可读写
                            if (!checkWriteRead) {
                                StringBuilder builder = new StringBuilder();
                                builder.append("is not allowed to delete :");
                                builder.append(data);
                                Log.w(TAG, builder.toString());
                                continue;
                            }
                            //魅族手机,这里只是打印log
                            if (needDelProtection && isTrashed == 1) {
                                int cc = count + 1;
                                StringBuilder builder = new StringBuilder();
                                builder.append("@_@ deleteInternal file has trashed already count = ");
                                builder.append(cc);
                                Log.d(TAG, builder.toString());
                            }

                            //魅族手机关键的地方------------------begin---------------------
                            final String callingPkg = getCallingPackageOrSelf();
                            File ff = new File(data);
                            boolean isDir = ff.isDirectory();
                            final String topDir = getTopLevelDir(data, isDir);
                            boolean isSelfDir = isAppSelfFile(callingPkg, topDir, data);
                            if (!isSelfDir) {
                                int i = handleTrashOneItem(data, id, mediaType, mimeType, size, delArrays);
                                count += i;
                            }
                            //魅族手机关键的地方----------------end-----------------------
                            //魅族手机--------end--------------

final boolean isFilesTable = qb.getTables().equals(“files”);一般返回true,所以会走进if语句中

接下来看看关键的地方,在这个地方会在某些条件下填充之前提到的delArrays
                            //魅族手机关键的地方------------------begin---------------------
                            final String callingPkg = getCallingPackageOrSelf();
                            File ff = new File(data);
                            boolean isDir = ff.isDirectory();
                            final String topDir = getTopLevelDir(data, isDir);
                            boolean isSelfDir = isAppSelfFile(callingPkg, topDir, data);
                            if (!isSelfDir) {
                                int i = handleTrashOneItem(data, id, mediaType, mimeType, size, delArrays);
                                count += i;
                            }
                            //魅族手机关键的地方----------------end-----------------------

继续以/storage/emulated/0/libs/3.txt为例, topDir返回libs

另外看到isAppSelfFile函数如果返回false,则会执行handleTrashOneItem,该函数会填充delArrays

isAppSelfFile我们后面再讲,不过可以明显猜出来,这个函数针对libs这个topDir肯定返回false, 所以肯定会调用handleTrashOneItem

handleTrashOneItem函数
    private static int handleTrashOneItem(String path, long id, int mediaType, String mimeType, long size, ArrayList<ContentValues> paramArrayList) {
        Uri uri = ContentUris.withAppendedId(FileUtils.getContentUriForPath(path), id);
        ContentValues contentValues1 = new ContentValues();
        contentValues1.put("is_trashed", Integer.valueOf(1));
        ContentValues contentValues2 = new ContentValues();
        contentValues2.put("date_deleted", Long.valueOf(System.currentTimeMillis() / 1000L));
        int i = update(uri, contentValues1, null, null);
        //更新数据库成功,则进入if语句
        if (i > 0) {
            contentValues2.put("_id", Long.valueOf(id));
            contentValues2.put("_data", path);
            contentValues2.put("_display_name", FileUtils.extractDisplayName(path));
            contentValues2.put("media_type", Integer.valueOf(mediaType));
            contentValues2.put("mime_type", mimeType);
            contentValues2.put("_size", Long.valueOf(size));
            //关键地方。。。这里填充了ContentValues2,代表要删除的文件信息
            paramArrayList.add(contentValues2);
        }
        return i;
    }
魅族手机发送通知消息的源头在这里就露出来马脚了
private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
            throws MediaProvider.FallbackException {
        .......
        .......
				if (isFilesTable && !isCallingPackageSelf()) {
                com.android.providers.media.util.Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
                        getCallingPackageOrSelf(), count);

                //魅族手机----begin
                if (delArrays.size() > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    final String callingPkg = getCallingPackageOrSelf();
                    ArrayList<ContentValues> pkgDelArrays = sMapDeleteList.get(callingPkg);
                    if (pkgDelArrays == null) {
                        ArrayList<ContentValues> clonedArr = (ArrayList<ContentValues>) delArrays.clone();
                        sMapDeleteList.put(callingPkg, clonedArr);
                    } else {
                        ArrayList<ContentValues> arrays = pkgDelArrays;
                        //删除重复数据,更新sMapDeleteList
                    }

                    Message msg = mHandler.obtainMessage();
                    msg.what = 10;
                    msg.obj = callingPkg;
                    mHandler.sendMessageDelayed(msg, 100);
                }
                //魅族手机----end
            }

​ delArrays数组中有删除的条目,则会存入MediaProvider的sMapDeleteList map中,该map以callingpkg为key。 然后使用handler的message,该message就是发送通知栏消息的源头

MediaProvider->mHandler

看看反编译代码,一目了然

    this.mHandler = new Handler(Looper.getMainLooper()) {
        @SuppressLint({"HandlerLeak"})
        public void handleMessage(Message param1Message) {
          if (param1Message != null) {
            ....
            int i = param1Message.what;
            if (i != 0) {
              if (i == 10) {
                MediaProvider mediaProvider = MediaProvider.this;
                //魅族手机, 发送broadcast
                mediaProvider.sendDeleteBroadcasts(mediaProvider.getContext(), (String)param1Message.obj);
              } 
            } else {
              .....
          } 
          super.handleMessage(param1Message);
        }
      };
sendDeleteBroadcasts函数
  private void sendDeleteBroadcasts(Context paramContext, String paramString) {
    ArrayList arrayList = sMapDeleteList.get(paramString);
    ....
    ....
    //魅族手机...发送通知函数
    if (arrayList1.size() > 0)
      sendToNotification(paramContext, intent, paramString, bool, arrayList1); 
    if (arrayList2.size() > 0)
      sendToNotification(paramContext, intent, paramString, bool, arrayList2);
    //魅族手机, 发送完通知,则将callingpkg对应的delArrays删除掉
    sMapDeleteList.remove(paramString);
  }
sendToNotification函数
  private void sendToNotification(Context paramContext, Intent paramIntent, String paramString, boolean paramBoolean, ArrayList<ContentValues> paramArrayList) {
    Bundle bundle = new Bundle();
    bundle.putString("packageName", paramString);
    bundle.putBoolean("isFpsApp", paramBoolean);
    bundle.putParcelableArrayList("delete_list", paramArrayList);
    paramIntent.putExtras(bundle);
    
    //魅族手机。。。发送broadcast,接收包为com.meizu.safe
    paramIntent.setPackage("com.meizu.safe");
    paramContext.sendBroadcast(paramIntent);
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("@_@ sendToNotification packageName: ");
    stringBuilder.append(paramString);
    stringBuilder.append(", list: ");
    stringBuilder.append(Arrays.toString(paramArrayList.toArray()));
    Log.d("MediaProvider", stringBuilder.toString());
  }

此处可以看出函数名sendToNotification有点扯淡。。。。其实只是发个广播给“com.meizu.safe”包,该包会进行通知栏提示

先提前给个总结,何时删除文件不会发送通知栏消息

  1. 删除的文件匹配到MEDIA_SCANNER
  2. 删除的文件匹配到AUDIO_PLAYLISTS_ID_MEMBERS_ID
  3. userdata分区,即/sdcard/Android/data/pkg与/data/data/pkg下面的文件删除不会调用fuse文件,所以也不会回调deleteFileForFuse
  4. isAppSelfFile返回为true

那么isAppSelfFile能不能永久返回true,这样不就可以豁免通知栏警告消息了嘛!!!

isAppSelfFile分析

isAppSelfFile函数

private static boolean isAppSelfFile(String callingPkg, String topDir, String filepath) {
    StringBuilder stringBuilder2 = new StringBuilder();
    stringBuilder2.append("@_@ isAppSelfFile pkg: ");
    stringBuilder2.append(callingPkg);
    stringBuilder2.append(", topName: ");
    stringBuilder2.append(topDir);
    stringBuilder2.append(", path: ");
    stringBuilder2.append(filepath);
    Log.d("MediaProvider", stringBuilder2.toString());
    boolean bool = false;
    try {
        Class<?> clazz = Class.forName("meizu.security.FlymePermissionManager");
        if (checkPkgForExclusiveDirectoryMethod == null)
            checkPkgForExclusiveDirectoryMethod = clazz.getMethod("checkPkgForExclusiveDirectory", new Class[] { String.class, String.class, String.class });
        int i = ((Integer)checkPkgForExclusiveDirectoryMethod.invoke((Object)null, new Object[] { callingPkg, topDir, filepath })).intValue();
        if (i == 0)
            //魅族手机,只有这里返回了true
            bool = true;
    } catch (Exception exception) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("MZSTORAGE-isAppSelfFile ");
        stringBuilder.append(exception.getMessage());
        Log.d("MediaProvider", stringBuilder.toString());
    }
    StringBuilder stringBuilder1 = new StringBuilder();
    stringBuilder1.append("@_@ isAppSelfFile appSelf = ");
    stringBuilder1.append(bool);
    Log.d("MediaProvider", stringBuilder1.toString());
    return bool;
}

通过上述反编译的函数中,只有在反射checkPkgForExclusiveDirectory函数并且返回值为0的时候才会返回true

meizu.security.FlymePermissionManager–>checkPkgForExclusiveDirectory函数

public static int checkPkgForExclusiveDirectory(String callingPkg, String topDir, String filePath) {
    try {
        return IFlymePermissionService.Stub.asInterface(ServiceManager.getService("flyme_permission")).checkPkgForExclusiveDirectory(callingPkg, topDir, filePath);
    } catch (Exception exception) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("checkPkgForExclusiveDirectory: ");
        stringBuilder.append(exception);
        Log.w("FlymePermissionManager", stringBuilder.toString());
        return 0;
    }
}

该函数使用了flyme_permission服务,该服务运行在system_server中,代码在services.jar包中

checkPkgForExclusiveDirectory函数(server端)

public int checkPkgForExclusiveDirectory(String pkg, String topDir, String filepath) {
    String str = topDir;
    if (topDir != null)
        str = topDir.toLowerCase();
    if (DEBUG) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("getPkgForExclusiveDirectory callerPkg ");
        stringBuilder.append(pkg);
        stringBuilder.append("   topName : ");
        stringBuilder.append(str);
        stringBuilder.append(" path: ");
        stringBuilder.append(filepath);
        Log.e("PermissionControl", stringBuilder.toString());
    }
    //魅族手机使用了mPackagesDefault该map去维护app self目录(Exclusive表示豁免目录)
    Iterator<OpEntry> iterator = this.mPackagesDefault.values().iterator();
    while (iterator.hasNext()) {
        OpEntry opEntry = iterator.next();
        final String pkgName = opEntry.packageName;
        if (TextUtils.equals(pkg, pkgName))
            for (Map.Entry<String, DirEntry> entry : opEntry.dirOp.entrySet()) {
                DirEntry dirEntry = (DirEntry)entry.getValue();
                String str1 = (String)entry.getKey();
                if (dirEntry.tag == 0 && str != null && str.equalsIgnoreCase(str1)) {
                    StringBuilder stringBuilder = new StringBuilder();
                    stringBuilder.append("checkPkgForExclusiveDirectory find return allow   ");
                    stringBuilder.append(pkg);
                    stringBuilder.append(" access  topName : ");
                    stringBuilder.append(str);
                    stringBuilder.append(" dir: ");
                    stringBuilder.append(str1);
                    stringBuilder.append(" path: ");
                    stringBuilder.append(filepath);
                    stringBuilder.append("  which owner is :");
                    stringBuilder.append(topDir);
                    Log.e("PermissionControl", stringBuilder.toString());
                    //魅族手机,此处返回0
                    return 0;
                }
            }
    }
    return 1;
}


public HashMap<String, OpEntry> mPackagesDefault = new HashMap<String, OpEntry>();

关注何时返回0.

该函数中使用了mPackagesDefault进行了遍历,先看几个关键的class吧

OpEntry.class

CaseInsensitiveMap.class

DirEntry.class

这几个class后面有具体代码,大家可以先看后面的代码再继续看当前的分析。

  1. 遍历后拿到OpEntry, 然后判断OpEntry.packageName是否等于三方app的包名
  2. 遍历OpEntry, key如果为topDir(例子中为libs),并且value即dirEntry.tag=0则返回0

那么mPackagesDefault能不能被我们的app操作呢?答案是-------可以的

我们看看mPackagesDefault何时进行put操作的

public void wirteTopDirAccessPermissionDefault(String pkg, String topDir, int paramInt1, int tag, int paramInt3) {
    DirEntry dirEntry;
    enforceCheckPermission();
    String ignoreDir = topDir;
    if (topDir != null)
        ignoreDir = topDir.toLowerCase();
    if (this.mPackagesDefault.containsKey(pkg)) {
        OpEntry opEntry = this.mPackagesDefault.get(pkg);
        if (opEntry.dirOp.containsKey(ignoreDir)) {
            dirEntry = opEntry.dirOp.get(ignoreDir);
            dirEntry.opValue = paramInt1;
            dirEntry.tag = tag;
            dirEntry.priority = paramInt3;
        } else {
            dirEntry = new DirEntry((pkg, ignoreDir, paramInt1, tag, paramInt3);
        }
        //魅族手机关键的地方
        opEntry.dirOp.put(ignoreDir, dirEntry);
    } else {
        byte b;
        try {
            b = this.mContext.getPackageManager().getPackageUid(pkg, 0);
        } catch (android.content.pm.PackageManager.NameNotFoundException nameNotFoundException) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("Couldn't get uid for ");
            stringBuilder.append((String)dirEntry);
            stringBuilder.append(" ");
            stringBuilder.append(nameNotFoundException.getMessage());
            Log.d("PermissionControl", stringBuilder.toString());
            b = -1;
        }
        OpEntry opEntry = new OpEntry(b, (String)dirEntry);
        DirEntry dirEntry1 = new DirEntry((String)dirEntry, ignoreDir, paramInt1, tag, paramInt3);
        opEntry.dirOp.put(ignoreDir, dirEntry1);
        //魅族手机关键的地方
        this.mPackagesDefault.put(dirEntry, opEntry);
    }
}

wirteTopDirAccessPermissionDefault该函数是可以通过binder调用执行的,所以我们可以在自己的app端通过反射调用flame_permission服务进行设置,进而保证app进行文件删除操作,豁免通知栏警告。

OpEntry.class
  public static final class OpEntry {
    FlymePermissionService.CaseInsensitiveMap dirOp = new FlymePermissionService.CaseInsensitiveMap();
    
    public String packageName;
    
    public int uid;
    
    public OpEntry(int param1Int, String param1String) {
      this.uid = param1Int;
      this.packageName = param1String;
    }
  }
CaseInsensitiveMap.class
  static class CaseInsensitiveMap extends HashMap<String, DirEntry> {
    public FlymePermissionService.DirEntry get(String param1String) {
      return (FlymePermissionService.DirEntry)get(param1String.toLowerCase());
    }
    
    public FlymePermissionService.DirEntry put(String param1String, FlymePermissionService.DirEntry param1DirEntry) {
      return super.put(param1String.toLowerCase(), param1DirEntry);
    }
  }
DirEntry.class
  public static final class DirEntry {
    public long accessTime;
    
    public String dirName;
    
    public int opValue;
    
    public String pkg;
    
    public int priority;
    
    public long rejectTime;
    
    public int tag;
    
    public DirEntry(String param1String1, String param1String2, int param1Int1, int param1Int2, int param1Int3) {
      this.pkg = param1String1;
      this.dirName = param1String2;
      this.opValue = param1Int1;
      this.tag = param1Int2;
      this.priority = param1Int3;
    }
  }

总结:魅族手机文件删除监控,可以通过flyme_permission服务调用wirteTopDirAccessPermissionDefault函数,设置豁免的目录

另外,“flyme.permission.READ_STORAGE”,可能需要申请此权限。。。

final String pkg = "com.example.demo"
final File ignoreFile = new File("/storage/emulated/0/libs/3.txt");
boolean isDir = ignoreFile.isDirectory();

final String topDir = getTopLevelDir(ignoreFile, isDir);/*MediaProvider.getTopLevelDir,可以逆向自己在app中实现*/

反射拿到binderProxy->meizu.security.IFlymePermissionService.Stub
  
反射调用binderProxy->writeTopDirAccessPermissionDefault(pkg, topDir,, 0,);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值