魅族手机文件删除后通知栏警告流程分析上
先将魅族手机中的关键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”包,该包会进行通知栏提示
先提前给个总结,何时删除文件不会发送通知栏消息
- 删除的文件匹配到MEDIA_SCANNER
- 删除的文件匹配到AUDIO_PLAYLISTS_ID_MEMBERS_ID
- userdata分区,即/sdcard/Android/data/pkg与/data/data/pkg下面的文件删除不会调用fuse文件,所以也不会回调deleteFileForFuse
- 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后面有具体代码,大家可以先看后面的代码再继续看当前的分析。
- 遍历后拿到OpEntry, 然后判断OpEntry.packageName是否等于三方app的包名
- 遍历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, ?);