- * FileProvider适配 - * content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/ - * content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/ - */ - private static String getFilePathFromContentUri(Context context, Uri uri) { - if (null == uri) return null; - String data = null; - - String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME}; - Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null); - if (null != cursor) { - if (cursor.moveToFirst()) { - int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); - if (index > -1) { - data = cursor.getString(index); - } else { - int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); - String fileName = cursor.getString(nameIndex); - data = getPathFromInputStreamUri(context, uri, fileName); - } - } - cursor.close(); - } - return data; - } - - /** - * 用流拷贝文件一份到自己APP私有目录下 - * - * @param context - * @param uri - * @param fileName - */ - private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) { - InputStream inputStream = null; + String scheme = uri.getScheme(); String filePath = null; - - if (uri.getAuthority() != null) { - try { - inputStream = context.getContentResolver().openInputStream(uri); - File file = createTemporalFileFrom(context, inputStream, fileName); - filePath = file.getPath(); - - } catch (Exception e) { - } finally { - try { - if (inputStream != null) { - inputStream.close(); - } - } catch (Exception e) { - } - } + // 按 Uri Scheme 分类处理 + if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { + LogUtils.d(TAG, "getFilePathFromUri:Scheme=content,执行ContentUri转换"); + filePath = getFilePathFromContentUri(context, uri); + } else if (ContentResolver.SCHEME_FILE.equals(scheme)) { + LogUtils.d(TAG, "getFilePathFromUri:Scheme=file,直接转换路径"); + filePath = new File(uri.getPath()).getAbsolutePath(); + } else { + LogUtils.w(TAG, "getFilePathFromUri:未知Scheme=" + scheme + ",转换失败"); } + LogUtils.d(TAG, "=== getFilePathFromUri 调用 end,结果:" + filePath + " ==="); return filePath; } - - public static Uri getUriForFile(Context context, String filePath) { - // 1. 打印传入的文件路径 - LogUtils.d(TAG, "getUriForFile -> 传入路径:" + filePath); - if (filePath == null || filePath.isEmpty()) { - LogUtils.e(TAG, "getUriForFile -> 传入路径为空"); - return null; - } - File file = new File(filePath); - // 2. 打印File对象的绝对路径和存在性 - LogUtils.d(TAG, "getUriForFile -> 文件绝对路径:" + file.getAbsolutePath()); - LogUtils.d(TAG, "getUriForFile -> 文件是否存在:" + file.exists()); - LogUtils.d(TAG, "getUriForFile -> 是否为目录:" + file.isDirectory()); + /** + * 文件路径转 Uri(核心方法,适配 Android7.0+ FileProvider,API29-30兼容) + * @param context 上下文(非空) + * @param filePath 真实文件路径(非空) + * @return 安全 Uri(转换失败返回 null) + */ + public static Uri getUriForFile(Context context, String filePath) { + LogUtils.d(TAG, "=== getUriForFile(路径版)调用 start ==="); + // 1. 基础参数校验 + if (context == null) { + LogUtils.e(TAG, "getUriForFile:Context 为空,转换失败"); + return null; + } + if (filePath == null || filePath.isEmpty()) { + LogUtils.e(TAG, "getUriForFile:文件路径为空,转换失败"); + return null; + } - // 3. 合法性校验 - if (!file.exists() || file.isDirectory()) { - LogUtils.e(TAG, "getUriForFile -> 非法路径:文件不存在或为目录"); - return null; - } + // 2. File 对象初始化与校验 + File file = new File(filePath); + LogUtils.d(TAG, "getUriForFile:文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists()); + if (!file.exists() || file.isDirectory()) { + LogUtils.e(TAG, "getUriForFile:文件不存在或为目录,转换失败"); + return null; + } - // 4. 校验路径是否在配置的合法目录内 - String appFilesDir = context.getExternalFilesDir(null) != null ? context.getExternalFilesDir(null).getAbsolutePath() : "null"; - String publicPicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBell/"; - String internalFilesDir = context.getFilesDir().getAbsolutePath(); - String cacheDir = context.getCacheDir().getAbsolutePath(); + // 3. 合法路径校验(适配小米机型,避免FileProvider配置外路径) + if (!isPathInValidDir(context, file)) { + LogUtils.w(TAG, "getUriForFile:路径不在安全配置目录内,小米机型可能出现权限异常"); + } - String absolutePath = file.getAbsolutePath(); - boolean isInConfigDir = absolutePath.startsWith(appFilesDir) - || absolutePath.startsWith(publicPicDir) - || absolutePath.startsWith(internalFilesDir) - || absolutePath.startsWith(cacheDir); - LogUtils.d(TAG, "getUriForFile -> 路径是否在配置目录内:" + isInConfigDir); - if (!isInConfigDir) { - LogUtils.w(TAG, "getUriForFile -> 路径不在FileProvider配置范围内,可能导致异常"); - // 非强制拦截,保留原有逻辑,仅警告 - } + // 4. 调用重载方法生成 Uri + Uri uri = getUriForFile(context, file); + LogUtils.d(TAG, "=== getUriForFile(路径版)调用 end,结果:" + (uri != null ? uri.toString() : "null") + " ==="); + return uri; + } - return getUriForFile(context, file); - } + /** + * File 对象转 Uri(重载方法,直接接收File,内部安全适配) + * @param context 上下文(非空) + * @param file 待转换 File 对象(非空) + * @return 安全 Uri(转换失败返回 null) + */ + public static Uri getUriForFile(Context context, File file) { + LogUtils.d(TAG, "=== getUriForFile(File版)调用 start ==="); + // 1. 基础参数校验 + if (context == null) { + LogUtils.e(TAG, "getUriForFile:Context 为空,转换失败"); + return null; + } + if (file == null) { + LogUtils.e(TAG, "getUriForFile:File 对象为空,转换失败"); + return null; + } + LogUtils.d(TAG, "getUriForFile:文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists()); + if (!file.exists() || file.isDirectory()) { + LogUtils.e(TAG, "getUriForFile:文件不存在或为目录,转换失败"); + return null; + } - public static Uri getUriForFile(Context context, File file) { - if (context == null) { - LogUtils.e(TAG, "getUriForFile -> Context为空"); - return null; - } - if (file == null) { - LogUtils.e(TAG, "getUriForFile -> File对象为空"); - return null; - } + // 2. 按系统版本生成 Uri(API24+ 强制 FileProvider,适配小米机型) + Uri uri = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + LogUtils.d(TAG, "getUriForFile:Android7.0+,使用FileProvider生成Uri"); + String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX; + LogUtils.d(TAG, "getUriForFile:FileProvider Authority=" + authority); + try { + uri = FileProvider.getUriForFile(context, authority, file); + LogUtils.d(TAG, "getUriForFile:Content Uri生成成功=" + uri.toString()); + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "getUriForFile:FileProvider生成失败(小米机型常见原因:路径未配置/Authority不匹配)", e); + } + } else { + LogUtils.d(TAG, "getUriForFile:Android7.0以下,使用Uri.fromFile生成"); + uri = Uri.fromFile(file); + LogUtils.d(TAG, "getUriForFile:File Uri生成成功=" + uri.toString()); + } - // 1. 二次校验文件状态 - LogUtils.d(TAG, "getUriForFile(File) -> 文件路径:" + file.getAbsolutePath()); - if (!file.exists() || file.isDirectory()) { - LogUtils.e(TAG, "getUriForFile(File) -> 文件不存在或为目录"); - return null; - } + LogUtils.d(TAG, "=== getUriForFile(File版)调用 end ==="); + return uri; + } - // 2. 版本判断与Uri生成 - if (Build.VERSION.SDK_INT >= 24) { - LogUtils.d(TAG, "getUriForFile -> Android 7.0+,使用FileProvider生成Uri"); - try { - String authority = context.getPackageName() + ".fileprovider"; - LogUtils.d(TAG, "getUriForFile -> FileProvider authority:" + authority); - Uri uri = FileProvider.getUriForFile(context, authority, file); - LogUtils.d(TAG, "getUriForFile -> 生成Content Uri成功:" + uri.toString()); - return uri; - } catch (IllegalArgumentException e) { - LogUtils.e(TAG, "getUriForFile -> FileProvider生成Uri失败:路径未配置或权限不足", e); - return null; - } - } else { - LogUtils.d(TAG, "getUriForFile -> Android 7.0以下,使用Uri.fromFile生成Uri"); - Uri uri = Uri.fromFile(file); - LogUtils.d(TAG, "getUriForFile -> 生成File Uri成功:" + uri.toString()); - return uri; - } - } - - private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) - throws IOException { + // ====================== 私有辅助方法(内部逻辑封装,不对外暴露)====================== + /** + * ContentUri 转真实路径(适配 content:// 格式,处理小米机型特殊Uri) + * @param context 上下文 + * @param uri ContentUri(如:content://media/external/file/xxx) + * @return 真实文件路径(失败返回 null) + */ + private static String getFilePathFromContentUri(Context context, Uri uri) { + LogUtils.d(TAG, "getFilePathFromContentUri:Uri=" + uri.toString()); + String filePath = null; + Cursor cursor = null; + // Java7 语法:try-catch-finally 手动关闭Cursor,避免内存泄漏 + try { + // 查询字段:优先 DATA 字段,失败则通过文件名+流拷贝获取 + String[] queryColumns = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME}; + cursor = context.getContentResolver().query(uri, queryColumns, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + // 优先读取 DATA 字段(直接获取路径) + int dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); + if (dataIndex != -1) { + filePath = cursor.getString(dataIndex); + LogUtils.d(TAG, "getFilePathFromContentUri:从DATA字段获取路径=" + filePath); + } else { + // DATA 字段为空,通过流拷贝到私有目录获取路径(小米机型特殊场景适配) + int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); + String fileName = cursor.getString(nameIndex); + LogUtils.d(TAG, "getFilePathFromContentUri:DATA字段为空,通过流拷贝获取,文件名=" + fileName); + filePath = getPathFromInputStreamUri(context, uri, fileName); + } + } + } catch (Exception e) { + LogUtils.e(TAG, "getFilePathFromContentUri:查询失败", e); + } finally { + // 强制关闭Cursor,避免资源泄漏(Java7 必须手动处理) + if (cursor != null) { + try { + cursor.close(); + } catch (Exception e) { + LogUtils.e(TAG, "getFilePathFromContentUri:关闭Cursor失败", e); + } + } + } + return filePath; + } + + /** + * 流拷贝获取路径(适配无 DATA 字段的 ContentUri,小米机型特殊Uri兼容) + * 将目标文件拷贝到应用私有缓存目录,返回拷贝后的路径 + * @param context 上下文 + * @param uri ContentUri + * @param fileName 文件名 + * @return 拷贝后的文件路径(失败返回 null) + */ + private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) { + LogUtils.d(TAG, "getPathFromInputStreamUri:开始流拷贝,文件名=" + fileName); + InputStream inputStream = null; + OutputStream outputStream = null; File targetFile = null; + try { + // 1. 打开输入流(读取Uri对应文件) + inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + LogUtils.e(TAG, "getPathFromInputStreamUri:输入流打开失败"); + return null; + } + // 2. 创建目标文件(应用私有缓存目录,无权限限制) + targetFile = new File(context.getExternalCacheDir(), fileName); + // 若文件已存在,先删除(避免覆盖导致格式异常) + if (targetFile.exists()) { + boolean deleteSuccess = targetFile.delete(); + LogUtils.d(TAG, "getPathFromInputStreamUri:删除已存在文件,结果=" + deleteSuccess); + } + + // 3. 流拷贝(Java7 手动处理流,避免 try-with-resources) + outputStream = new FileOutputStream(targetFile); + byte[] buffer = new byte[8 * 1024]; // 8KB 缓冲区,平衡效率与内存 + int readLength; + while ((readLength = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, readLength); + } + outputStream.flush(); + LogUtils.d(TAG, "getPathFromInputStreamUri:流拷贝成功,路径=" + targetFile.getAbsolutePath()); + } catch (Exception e) { + LogUtils.e(TAG, "getPathFromInputStreamUri:流拷贝失败", e); + // 拷贝失败,删除临时文件 + if (targetFile != null && targetFile.exists()) { + targetFile.delete(); + } + targetFile = null; + } finally { + // 强制关闭流,避免资源泄漏(Java7 必须手动关闭) + try { + if (outputStream != null) { + outputStream.close(); + } + } catch (IOException e) { + LogUtils.e(TAG, "getPathFromInputStreamUri:关闭输出流失败", e); + } + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + LogUtils.e(TAG, "getPathFromInputStreamUri:关闭输入流失败", e); + } + } + return targetFile != null ? targetFile.getAbsolutePath() : null; + } + + /** + * 校验路径是否在安全目录内(适配API29-30+小米机型,避免FileProvider权限异常) + * 仅允许:应用私有目录、缓存目录、应用专属公共目录 + * @param context 上下文 + * @param file 待校验文件 + * @return true=安全路径,false=非安全路径 + */ + private static boolean isPathInValidDir(Context context, File file) { + String absolutePath = file.getAbsolutePath(); + // 1. 应用外部私有目录(API29+ 推荐,无权限限制) + String externalPrivateDir = context.getExternalFilesDir(null) != null + ? context.getExternalFilesDir(null).getAbsolutePath() + : ""; + // 2. 应用内部私有目录(无权限限制) + String internalPrivateDir = context.getFilesDir().getAbsolutePath(); + // 3. 应用缓存目录(无权限限制) + String cacheDir = context.getCacheDir().getAbsolutePath(); + // 4. 应用专属公共目录(API29+ 适配,替代废弃的 getExternalStoragePublicDirectory) + String appPublicDir = Environment.getExternalStorageDirectory().getAbsolutePath() + + File.separator + Environment.DIRECTORY_PICTURES + + File.separator + APP_PUBLIC_PIC_DIR; + + // 校验路径是否在安全目录内(小米机型必须严格校验,否则FileProvider会抛异常) + boolean isInValidDir = absolutePath.startsWith(externalPrivateDir) + || absolutePath.startsWith(internalPrivateDir) + || absolutePath.startsWith(cacheDir) + || absolutePath.startsWith(appPublicDir); + + LogUtils.d(TAG, "isPathInValidDir:外部私有目录=" + externalPrivateDir + + ",公共目录=" + appPublicDir + + ",校验结果=" + isInValidDir); + return isInValidDir; + } + + /** + * 流拷贝创建临时文件(内部辅助,封装拷贝逻辑) + * @param context 上下文 + * @param inputStream 输入流 + * @param fileName 文件名 + * @return 临时文件(失败返回 null) + * @throws IOException 流操作异常 + */ + private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) throws IOException { + File targetFile = null; if (inputStream != null) { - int read; byte[] buffer = new byte[8 * 1024]; - //自己定义拷贝文件路径 + int readLength; targetFile = new File(context.getExternalCacheDir(), fileName); if (targetFile.exists()) { targetFile.delete(); } OutputStream outputStream = new FileOutputStream(targetFile); - - while ((read = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, read); + while ((readLength = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, readLength); } outputStream.flush(); - - try { - outputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } + outputStream.close(); } - return targetFile; } + /** + * 辅助:ContentUri 通过 MIME 类型获取后缀(精准匹配,不受文件名伪造影响) + * @param context 上下文 + * @param uri ContentUri + * @return 匹配的后缀(无匹配返回空字符串) + */ + private static String getSuffixFromContentUri(Context context, Uri uri) { + String mime = null; + try { + // 通过 ContentResolver 获取 Uri 对应的 MIME 类型(系统级匹配,最精准) + mime = context.getContentResolver().getType(uri); + LogUtils.d(TAG, "getSuffixFromContentUri:获取MIME类型=" + mime); + if (mime == null || mime.isEmpty()) { + // MIME 为空,尝试解析文件名兜底 + String fileName = getFileNameFromContentUri(context, uri); + return getSuffixFromFilePath(fileName); + } + // MIME 类型匹配后缀(优先完全匹配,再模糊匹配) + if (MIME_SUFFIX_MAP.containsKey(mime)) { + return MIME_SUFFIX_MAP.get(mime); + } + // 模糊匹配(如 image/* 匹配通用图片后缀,默认png) + if (mime.startsWith("image/")) { + return "png"; + } else if (mime.startsWith("video/")) { + return "mp4"; + } else if (mime.startsWith("audio/")) { + return "mp3"; + } else if (mime.startsWith("application/")) { + return "pdf"; + } + } catch (Exception e) { + LogUtils.e(TAG, "getSuffixFromContentUri:MIME解析失败,mime=" + mime, e); + } + // 所有方式失败,解析Uri路径兜底 + return getSuffixFromFilePath(uri.getPath()); + } + /** + * 辅助:从 ContentUri 获取文件名(MIME 解析失败时兜底) + * @param context 上下文 + * @param uri ContentUri + * @return 文件名(失败返回空字符串) + */ + private static String getFileNameFromContentUri(Context context, Uri uri) { + Cursor cursor = null; + try { + String[] queryColumns = {MediaStore.MediaColumns.DISPLAY_NAME}; + cursor = context.getContentResolver().query(uri, queryColumns, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); + return cursor.getString(nameIndex); + } + } catch (Exception e) { + LogUtils.e(TAG, "getFileNameFromContentUri:查询失败", e); + } finally { + if (cursor != null) { + try { + cursor.close(); + } catch (Exception e) { + LogUtils.e(TAG, "getFileNameFromContentUri:关闭Cursor失败", e); + } + } + } + return ""; + } + + /** + * 辅助:从文件路径/文件名截取后缀(兜底方案,处理各种路径格式) + * @param path 文件路径/文件名 + * @return 截取的后缀(无后缀返回空字符串) + */ + private static String getSuffixFromFilePath(String path) { + if (path == null || path.isEmpty()) { + return ""; + } + // 处理路径中的分隔符(兼容 Windows/Android 路径格式) + path = path.replace("\\", "/"); + // 取最后一个 "/" 后的文件名(避免路径包含 "." 导致误判) + int lastSepIndex = path.lastIndexOf("/"); + if (lastSepIndex != -1 && lastSepIndex < path.length() - 1) { + path = path.substring(lastSepIndex + 1); + } + // 截取最后一个 "." 后的后缀(过滤无后缀/点开头/点结尾场景) + int lastDotIndex = path.lastIndexOf("."); + if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == path.length() - 1) { + return ""; + } + // 过滤后缀中的非法字符(仅保留字母/数字,避免特殊字符干扰) + String suffix = path.substring(lastDotIndex + 1).replaceAll("[^a-zA-Z0-9]", ""); + // 限制后缀长度(1-5位,避免超长伪造后缀) + return suffix.length() >= 1 && suffix.length() <= 5 ? suffix : ""; + } } +