重构权限申请模块,图片选择剪裁部分测试通过。

This commit is contained in:
2025-12-01 17:34:47 +08:00
parent 6538ebafef
commit ee4b0ca6d9
6 changed files with 1111 additions and 639 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Dec 01 07:58:43 GMT 2025
#Mon Dec 01 09:33:34 GMT 2025
stageCount=13
libraryProject=
baseVersion=15.11
publishVersion=15.11.12
buildCount=52
buildCount=59
baseBetaVersion=15.11.13

View File

@@ -1,14 +1,23 @@
package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.os.Environment;
import android.text.TextUtils;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import android.util.Log;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -570,7 +579,7 @@ public class BackgroundSourceUtils {
}
LogUtils.d(TAG, "【文件管理】裁剪相关临时文件清理完成(/Pictures/PowerBell下");
}
/**
* 适配原调用mBgSourceUtils.copyFile(new File(""), parentDir)
* 核心复用FileUtils支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景
@@ -620,5 +629,184 @@ public class BackgroundSourceUtils {
return "外部存储目录(非应用私有,权限受限)";
}
}
// ======================================== 核心实现:获取图片旋转角度 ========================================
/**
* 读取图片EXIF信息获取旋转角度适配JPEG/PNG等主流格式
* @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录)
* @return 旋转角度0/90/180/270无旋转返回0
*/
public int getImageRotateAngle(String imagePath) {
// 1. 入参校验(避免空指针/无效路径)
if (TextUtils.isEmpty(imagePath)) {
Log.e(TAG, "getImageRotateAngle: 图片路径为空");
return 0;
}
File imageFile = new File(imagePath);
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径:" + imagePath);
return 0;
}
InputStream inputStream = null;
try {
// 2. 读取图片EXIF信息优先用流读取避免文件占用
inputStream = new FileInputStream(imageFile);
ExifInterface exifInterface = new ExifInterface(inputStream);
// 3. 获取旋转角度标签兼容不同设备的EXIF字段
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
);
// 4. 解析旋转角度标准EXIF角度映射
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default: // 正常/翻转等其他情况均视为0度
return 0;
}
} catch (IOException e) {
// 兼容异常场景如图片无EXIF信息、格式不支持如WebP
Log.w(TAG, "getImageRotateAngle: 读取EXIF异常路径" + imagePath + ",错误:" + e.getMessage());
return 0;
} finally {
// 5. 关闭流资源(避免内存泄漏/文件占用)
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误:" + e.getMessage());
}
}
}
}
// ======================================== 图片处理核心方法(压缩/裁剪/保存) ========================================
/**
* 压缩图片并保存(核心修复:路径非空校验+兜底路径Java7 手动管理流)
*/
public void compressQualityToRecivedPicture(Bitmap bitmap) {
// 兼容裁剪等旧调用:从工具类获取默认压缩路径,转发至重载函数
String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath();
compressQualityToRecivedPicture(bitmap, defaultCompressPath);
}
/**
* 重载方法指定路径压缩图片并保存修复压缩后同步路径到预览Bean
* 适配场景裁剪后生成压缩图强制绑定路径到预览Bean避免路径错位
* @param bitmap 待压缩的Bitmap裁剪后的缩放图
* @param targetCompressPath 强制指定的压缩目标路径从预览Bean获取/生成)
*/
public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) {
LogUtils.d(TAG, "【压缩启动】开始压缩图片指定路径Bitmap状态" + (bitmap != null && !bitmap.isRecycled()));
if (bitmap == null || bitmap.isRecycled()) {
ToastUtils.show("压缩失败:图片为空");
LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收");
return;
}
OutputStream outStream = null;
FileOutputStream fos = null;
try {
String scaledCompressFilePath = targetCompressPath;
// 兜底:若传入路径为空,生成临时压缩路径(兼容异常场景)
if (TextUtils.isEmpty(scaledCompressFilePath)) {
LogUtils.e(TAG, "【压缩异常】指定路径为空,使用临时兜底路径");
File tempDir = new File(App.getTempDirPath(), "PreviewCompress"); // 多包名环境下临时目录隔离
if (!tempDir.exists()) {
tempDir.mkdirs();
FileUtils.copyFile(new File(""), tempDir); // 复用原有目录创建逻辑
}
scaledCompressFilePath = new File(tempDir, "preview_compress_" + System.currentTimeMillis() + ".jpg").getAbsolutePath();
LogUtils.d(TAG, "【压缩兜底】临时路径:" + scaledCompressFilePath);
}
File compressFile = new File(scaledCompressFilePath);
LogUtils.d(TAG, "【压缩配置】目标路径:" + scaledCompressFilePath + "Bitmap原始大小" + bitmap.getByteCount() / 1024 + "KB");
// 确保父目录存在兼容Android 10+分区存储,多包名路径适配)
File parentDir = compressFile.getParentFile();
if (parentDir == null) {
LogUtils.e(TAG, "【压缩异常】父目录为空,无法创建文件");
ToastUtils.show("压缩失败:路径无效");
return;
}
if (!parentDir.exists()) {
parentDir.mkdirs();
FileUtils.copyFile(new File(""), parentDir);
LogUtils.d(TAG, "【压缩准备】目录已创建:" + parentDir.getAbsolutePath());
}
// 清理旧的压缩文件(避免文件残留)
if (compressFile.exists()) {
clearOldFileByExternal(compressFile, "旧压缩文件");
}
compressFile.createNewFile();
// 写入压缩图质量80平衡清晰度和内存
fos = new FileOutputStream(compressFile);
outStream = new BufferedOutputStream(fos);
boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream);
outStream.flush();
// 强制同步到磁盘(避免异步写入导致控件读取不到文件)
if (fos != null) {
try {
fos.getFD().sync();
LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘");
} catch (IOException e) {
LogUtils.w(TAG, "【压缩保存】sync()失败flush()兜底:" + e.getMessage());
outStream.flush();
}
}
LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + compressFile.length() / 1024 + "KB");
// 关键修复压缩成功后强制同步路径到预览Bean双重保障避免时序错位
if (compressSuccess) {
BackgroundBean previewBean = getPreviewBackgroundBean();
if (previewBean != null) {
previewBean.setBackgroundScaledCompressFilePath(scaledCompressFilePath);
saveSettings(); // 持久化配置,多包名环境下配置隔离
LogUtils.d(TAG, "【压缩路径同步】已绑定到预览Bean" + scaledCompressFilePath);
} else {
LogUtils.e(TAG, "【压缩路径同步失败】预览Bean为空");
}
} else {
ToastUtils.show("图片压缩失败");
LogUtils.e(TAG, "【压缩失败】Bitmap.compress()返回false");
}
} catch (IOException e) {
LogUtils.e(TAG, "【压缩异常】IO错误" + e.getMessage(), e);
ToastUtils.show("图片压缩失败");
} finally {
// 资源回收(避免内存泄漏)
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream" + e.getMessage());
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
LogUtils.e(TAG, "【流关闭失败】FileOutputStream" + e.getMessage());
}
}
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
}

View File

@@ -0,0 +1,214 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import java.util.ArrayList;
import java.util.List;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/01 16:05
* @Describe 权限申请工具类(单例)
* 核心特性:
* 1. 适配全Android版本6.0+ 动态权限 / 13+ 兼容)
* 2. 支持多包名场景(无硬编码包名)
* 3. 统一权限校验、申请、回调处理
* 4. 自带用户引导(拒绝权限+不再询问场景)
*/
public class PermissionUtils {
public static final String TAG = "PermissionUtils";
// 存储权限请求码与Activity保持一致避免冲突
public static final int STORAGE_PERMISSION_REQUEST2 = 100;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101;
// 单例实例(双重校验锁,线程安全)
private static volatile PermissionUtils sInstance;
// 私有构造(禁止外部实例化)
private PermissionUtils() {}
/**
* 获取单例实例(适配多包名,无硬编码)
*/
public static PermissionUtils getInstance() {
if (sInstance == null) {
synchronized (PermissionUtils.class) {
if (sInstance == null) {
sInstance = new PermissionUtils();
}
}
}
return sInstance;
}
// ======================================== 存储权限核心方法 ========================================
/**
* 检查并申请存储权限统一入口适配全Android版本
* @param activity 上下文(用于权限申请和弹窗)
* @return true权限已全部获取false需要申请权限
*/
public boolean checkAndRequestStoragePermission(Activity activity) {
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁权限检查失败");
return false;
}
LogUtils.d(TAG, "【权限检查】开始检查存储权限Android版本" + Build.VERSION.SDK_INT);
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE适配所有版本避免READ_MEDIA_IMAGES找不到符号
String[] requiredPermissions = {
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE
};
// 筛选未授予的权限
List<String> needPermissions = new ArrayList<>();
for (String permission : requiredPermissions) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
needPermissions.add(permission);
}
}
// 无权限需要申请:触发动态申请
if (!needPermissions.isEmpty()) {
String[] permissionsArr = needPermissions.toArray(new String[0]);
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2);
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr));
return false;
}
// 所有权限已授予
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
return true;
}
/**
* 处理存储权限申请回调统一逻辑无需在Activity中重复编写
* @param activity 上下文
* @param requestCode 请求码匹配STORAGE_PERMISSION_REQUEST
* @param permissions 申请的权限数组
* @param grantResults 权限授予结果数组
* @return true回调已处理false非当前工具类的权限回调
*/
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
// 过滤非存储权限回调
if (requestCode != STORAGE_PERMISSION_REQUEST2) {
return false;
}
LogUtils.d(TAG, "【权限回调】处理存储权限回调requestCode" + requestCode);
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【权限回调】Activity为空或已销毁回调处理终止");
return true;
}
// 校验所有权限是否授予
boolean allGranted = true;
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// 全部授予:提示用户重新操作
ToastUtils.show(activity.getString(R.string.permission_grant_success));
LogUtils.d(TAG, "【权限回调】所有存储权限已授予");
} else {
// 部分/全部拒绝:判断是否勾选“不再询问”
boolean shouldShowRationale = false;
for (String permission : permissions) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
shouldShowRationale = true;
break;
}
}
if (shouldShowRationale) {
// 未勾选“不再询问”:弹窗引导重新申请
showPermissionRationaleDialog(activity);
} else {
// 已勾选“不再询问”:引导用户去设置页开启
showPermissionSettingDialog(activity);
}
LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale);
}
return true;
}
// ======================================== 辅助方法(私有,封装细节) ========================================
/**
* 弹窗:未勾选“不再询问”时,提示用户授予权限
*/
private void showPermissionRationaleDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_title))
.setMessage(activity.getString(R.string.permission_storage_rationale))
.setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 重新申请权限
checkAndRequestStoragePermission(activity);
}
})
.setNegativeButton(activity.getString(R.string.cancel), null)
.show();
}
/**
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限
*/
private void showPermissionSettingDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.permission_denied_title))
.setMessage(activity.getString(R.string.permission_storage_setting_guide))
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 跳转应用设置页(适配多包名,动态获取当前包名)
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivity(intent);
}
})
.setNegativeButton(activity.getString(R.string.cancel), null)
.show();
}
// ======================================== 扩展:其他权限方法(可选) ========================================
/**
* 检查单个权限是否已授予(通用方法,可复用)
*/
public boolean isPermissionGranted(Activity activity, String permission) {
if (activity == null || TextUtils.isEmpty(permission)) {
return false;
}
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* 申请单个权限(通用方法,可复用)
*/
public void requestSinglePermission(Activity activity, String permission, int requestCode) {
if (activity == null || TextUtils.isEmpty(permission)) {
return;
}
if (!isPermissionGranted(activity, permission)) {
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
}
}
}

View File

@@ -13,6 +13,9 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import java.io.File;
import android.widget.ImageView.ScaleType;
import android.text.TextUtils;
import androidx.annotation.Nullable;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -97,6 +100,10 @@ public class BackgroundView extends RelativeLayout {
this.addView(ivBackground); // 添加到父容器(控件本身)
}
public void setPixelColor(int nBackgroundColor) {
ivBackground.setBackgroundColor(nBackgroundColor);
}
/**
* 【对外提供】重新加载正式背景图片从正式Bean获取路径
* 用于:退出预览模式、恢复默认背景、正式背景更新后刷新
@@ -111,31 +118,126 @@ public class BackgroundView extends RelativeLayout {
}
/**
* 【对外提供】重新加载预览背景图片从预览Bean获取路径
* 修复增加isUseBackgroundFile校验确保启用背景图时才加载
* 重新加载预览背景修复强制读取预览Bean最新路径+多重有效性校验
* 作用:控件加载图片时,优先用压缩图路径,失败则用原图路径,均失败则显示默认图
*/
public void reloadPreviewBackground() {
LogUtils.d(TAG, "=== 开始重新加载预览背景 ===");
isPreviewMode = true; // 标记为预览模式
// 从工具类获取最新预览背景Bean
BackgroundBean previewBean = backgroundSourceUtils.getPreviewBackgroundBean();
// 修复:若未启用背景图,直接显示透明背景(避免无效加载)
if (!previewBean.isUseBackgroundFile()) {
LogUtils.d(TAG, "预览Bean未启用背景图isUseBackgroundFile=false显示透明背景");
setDefaultTransparentBackground();
LogUtils.d(TAG, "=== 开始加载预览背景(路径校验版)===");
// 关键直接从工具类获取预览Bean最新路径避免使用缓存路径
BackgroundSourceUtils bgSourceUtils = BackgroundSourceUtils.getInstance(getContext());
BackgroundBean previewBean = bgSourceUtils.getPreviewBackgroundBean();
if (previewBean == null) {
LogUtils.e(TAG, "【加载失败】预览Bean为空显示默认图");
setDefaultBackground();
return;
}
// 优先加载压缩图,无则加载原图(保持原逻辑)
String backgroundPath;
if (previewBean.isUseBackgroundScaledCompressFile()) {
backgroundPath = backgroundSourceUtils.getPreviewBackgroundScaledCompressFilePath();
} else {
backgroundPath = backgroundSourceUtils.getPreviewBackgroundFilePath();
// 1. 优先加载压缩图路径(预览首选,节省内存)
String compressPath = previewBean.getBackgroundScaledCompressFilePath();
File compressFile = checkFileValidity(compressPath); // 校验文件有效性
if (compressFile != null) {
loadBitmapAndDisplay(compressPath, "压缩图");
return;
}
// 加载并显示图片
loadAndSetImageViewBackground(backgroundPath);
// 2. 兜底加载原图路径(压缩图失败时)
LogUtils.w(TAG, "【压缩图无效】尝试加载原图路径");
String originalPath = previewBean.getBackgroundFilePath();
File originalFile = checkFileValidity(originalPath);
if (originalFile != null) {
loadBitmapAndDisplay(originalPath, "原图");
return;
}
// 3. 终极兜底:显示默认图(避免白色)
LogUtils.e(TAG, "【加载失败】压缩图和原图均无效,显示默认图");
setDefaultBackground();
}
/**
* 新增:校验文件有效性(路径非空+文件存在+是文件+大小>100bytes
* @param filePath 待校验的图片路径
* @return 有效则返回File对象无效则返回null
*/
@Nullable
private File checkFileValidity(String filePath) {
if (TextUtils.isEmpty(filePath)) {
LogUtils.w(TAG, "【文件校验】路径为空");
return null;
}
File file = new File(filePath);
if (!file.exists()) {
LogUtils.w(TAG, "【文件校验】文件不存在:" + filePath);
return null;
}
if (!file.isFile()) {
LogUtils.w(TAG, "【文件校验】不是文件:" + filePath);
return null;
}
if (file.length() <= 100) {
LogUtils.w(TAG, "【文件校验】文件过小(无效):" + filePath + ",大小:" + file.length() + "bytes");
return null;
}
LogUtils.d(TAG, "【文件校验】有效:" + filePath + ",大小:" + file.length() + "bytes");
return file;
}
/**
* 新增加载Bitmap并显示统一处理Bitmap有效性
* @param imagePath 图片路径
* @param imageType 图片类型(压缩图/原图,用于日志区分)
*/
private void loadBitmapAndDisplay(String imagePath, String imageType) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
// 校验Bitmap有效性避免宽高为0的无效Bitmap
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
LogUtils.e(TAG, "" + imageType + "加载失败】Bitmap无效空/宽高为0");
setDefaultBackground();
return;
}
// 强制设置ScaleType避免因缩放导致图片显示异常
ivBackground.setScaleType(ScaleType.CENTER_CROP);
// 移除默认背景色(避免白色覆盖图片)
setBackgroundColor(getResources().getColor(android.R.color.transparent));
// 显示图片并调整尺寸(复用原有调整逻辑)
ivBackground.setImageBitmap(bitmap);
adjustImageViewSize(bitmap); // 复用项目中原有尺寸调整方法
LogUtils.d(TAG, "" + imageType + "加载成功】宽高:" + bitmap.getWidth() + "x" + bitmap.getHeight());
}
/**
* 兜底:无默认图片时,用代码生成透明/纯色背景(避免报错)
*/
private void setDefaultBackground() {
ivBackground.setScaleType(ScaleType.CENTER_CROP);
// 方案1生成透明背景推荐避免白色
ivBackground.setBackgroundColor(getResources().getColor(android.R.color.transparent));
// 或 方案2生成白色背景根据界面需求选择
// ivBackground.setBackgroundColor(getResources().getColor(android.R.color.white));
// 或 方案3生成灰色背景更友好避免纯白/纯黑突兀)
// ivBackground.setBackgroundColor(getResources().getColor(android.R.color.darker_gray));
// 可选设置一个空的Bitmap避免ImageView显示空白
Bitmap emptyBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
ivBackground.setImageBitmap(emptyBitmap);
LogUtils.d(TAG, "【默认背景】使用代码生成透明背景(无图片资源)");
}
// 复用原有尺寸调整方法(若项目中已存在,无需重复添加)
private void adjustImageViewSize(Bitmap bitmap) {
// 此处保留项目中原有逻辑(如根据控件尺寸缩放图片等)
// 示例逻辑(仅供参考):
int viewWidth = getWidth();
int viewHeight = getHeight();
if (viewWidth > 0 && viewHeight > 0 && bitmap != null) {
// 计算缩放比例,适配控件尺寸
float scale = Math.min((float) viewWidth / bitmap.getWidth(), (float) viewHeight / bitmap.getHeight());
// 后续尺寸调整逻辑...
}
}
/**
* 【对外提供】预览指定路径的临时图片直接传入路径不依赖Bean
* 用于:临时预览本地图片、测试图片等场景
@@ -198,6 +300,22 @@ public class BackgroundView extends RelativeLayout {
LogUtils.d(TAG, "图片路径:" + imagePath + ",宽高比:" + imageAspectRatio);
}
/**
* 【新增工具函数】校验图片路径有效性(路径非空+文件存在+是文件+大小合理)
* 用于reloadPreviewBackground中压缩图/原始图的前置校验,避免无效加载
*/
private boolean isImagePathValid(String imagePath) {
if (imagePath == null || imagePath.isEmpty()) {
LogUtils.d(TAG, "图片路径为空,无效");
return false;
}
File imageFile = new File(imagePath);
// 校验:文件存在 + 是普通文件 + 大小>100字节避免空文件/损坏文件)
boolean isValid = imageFile.exists() && imageFile.isFile() && imageFile.length() > 100;
LogUtils.d(TAG, "图片路径校验:" + imagePath + ",是否有效:" + isValid + "(大小:" + imageFile.length() + "字节)");
return isValid;
}
/**
* 计算图片原始宽高比(宽/高)→ 控制不拉伸的核心
* @param file 图片文件(非空)

View File

@@ -31,4 +31,15 @@
<string name="subtitle_activity_pixelpicker">Pixel Picker</string>
<string name="subtitle_activity_about">About The APP</string>
<string name="msg_AOHPCTCSeekBar_ClearRecord">&gt;&gt;&gt;Seek 100% Right Is Clean Record.&gt;&gt;&gt;</string>
<!-- 权限申请相关字符串(统一管理,避免硬编码) -->
<string name="permission_title">权限申请</string>
<string name="permission_denied_title">权限被拒绝</string>
<string name="permission_grant_success">权限获取成功,请重新操作</string>
<string name="permission_storage_rationale">需要存储权限才能选择/拍照/裁剪图片,请授予权限</string>
<string name="permission_storage_setting_guide">存储权限已被拒绝且勾选“不再询问”,请前往设置页开启权限</string>
<string name="confirm">确定</string>
<string name="cancel">取消</string>
<string name="go_to_setting">去设置</string>
</resources>