魅族手机文件删除后通知栏警告流程分析上
先将魅族手机中的关键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;
}</