20251211_130438_907
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Thu Dec 11 00:57:37 GMT 2025
|
||||
#Thu Dec 11 04:59:14 GMT 2025
|
||||
stageCount=15
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.14
|
||||
buildCount=13
|
||||
buildCount=21
|
||||
baseBetaVersion=15.12.15
|
||||
|
||||
BIN
powerbell/src/main/assets/images/blank10x10.png
Normal file
BIN
powerbell/src/main/assets/images/blank10x10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 B |
@@ -47,6 +47,7 @@ import cc.winboll.studio.powerbell.utils.ImageCropUtils;
|
||||
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||
import cc.winboll.studio.powerbell.utils.AssetsCopyUtils;
|
||||
|
||||
public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
|
||||
|
||||
@@ -55,8 +56,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
private PermissionUtils mPermissionUtils;
|
||||
private BitmapCacheUtils mBitmapCache;
|
||||
|
||||
// 新增:手动定义 Android 13 对应的 SDK 版本号
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
// 新增:手动定义 Android 13 对应的 SDK 版本号
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
|
||||
public static final int REQUEST_SELECT_PICTURE = 0;
|
||||
public static final int REQUEST_TAKE_PHOTO = 1;
|
||||
@@ -111,10 +112,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
mBgSourceUtils.setCurrentSourceToPreview();
|
||||
}
|
||||
|
||||
//Uri uriSelectedImage = UriUtil.getUriForFile(this, new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundFilePath()));
|
||||
File fTestSourceExist = new File(mBgSourceUtils.getCurrentBackgroundBean().getBackgroundFilePath());
|
||||
boolean isCreateNewPreviewBean = !fTestSourceExist.exists();
|
||||
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(isCreateNewPreviewBean, mBgSourceUtils.getPreviewBackgroundBean());
|
||||
mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(mBgSourceUtils.getPreviewBackgroundBean());
|
||||
doubleRefreshPreview();
|
||||
LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成");
|
||||
}
|
||||
@@ -191,21 +189,21 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
};
|
||||
|
||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】选择图片");
|
||||
// 适配Android 13+权限(已替换为手动定义的常量)
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
if (mPermissionUtils.checkAndRequestMediaImagesPermission(BackgroundSettingsActivity.this, REQUEST_READ_MEDIA)) {
|
||||
launchImageSelector();
|
||||
}
|
||||
} else {
|
||||
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
|
||||
launchImageSelector();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【按钮点击】选择图片");
|
||||
// 适配Android 13+权限(已替换为手动定义的常量)
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
if (mPermissionUtils.checkAndRequestMediaImagesPermission(BackgroundSettingsActivity.this, REQUEST_READ_MEDIA)) {
|
||||
launchImageSelector();
|
||||
}
|
||||
} else {
|
||||
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
|
||||
launchImageSelector();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void launchImageSelector() {
|
||||
LogUtils.d(TAG, "【选图权限】已获取,启动选择器");
|
||||
@@ -437,7 +435,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
}
|
||||
|
||||
Bitmap photoBitmap = getTakePhotoBitmap(data);
|
||||
if (photoBitmap != null && !photoBitmap.isRecycled()) {
|
||||
if (isBitmapValid(photoBitmap)) {
|
||||
mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap);
|
||||
} else {
|
||||
ToastUtils.show("拍照图片为空");
|
||||
@@ -462,7 +460,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
if (mfTakePhoto != null && mfTakePhoto.exists()) {
|
||||
LogUtils.d(TAG, "【拍照Bitmap解析】从文件解析");
|
||||
Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto);
|
||||
if (photoBitmap != null && !photoBitmap.isRecycled()) {
|
||||
if (isBitmapValid(photoBitmap)) {
|
||||
LogUtils.d(TAG, "【拍照Bitmap解析】成功");
|
||||
return photoBitmap;
|
||||
} else {
|
||||
@@ -475,7 +473,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
if (data != null) {
|
||||
try {
|
||||
Bitmap thumbnailBitmap = (Bitmap) data.getParcelableExtra("data");
|
||||
if (thumbnailBitmap != null && !thumbnailBitmap.isRecycled()) {
|
||||
if (isBitmapValid(thumbnailBitmap)) {
|
||||
LogUtils.d(TAG, "【拍照Bitmap解析】从Intent获取成功");
|
||||
return thumbnailBitmap;
|
||||
} else {
|
||||
@@ -494,76 +492,57 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
}
|
||||
|
||||
private void handleSelectPictureResult(int resultCode, Intent data) {
|
||||
if (resultCode != RESULT_OK || data == null) {
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
if (resultCode != RESULT_OK || data == null) {
|
||||
handleOperationCancelOrFail();
|
||||
return;
|
||||
}
|
||||
|
||||
Uri selectedImage = data.getData();
|
||||
if (selectedImage == null) {
|
||||
ToastUtils.show("图片Uri为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "【选图回调】系统返回Uri : " + selectedImage.toString());
|
||||
Uri selectedImage = data.getData();
|
||||
if (selectedImage == null) {
|
||||
ToastUtils.show("图片Uri为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "【选图回调】系统返回Uri : " + selectedImage.toString());
|
||||
|
||||
// 1. 申请持久化读取权限
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
getContentResolver().takePersistableUriPermission(
|
||||
selectedImage,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
);
|
||||
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
|
||||
}
|
||||
// 1. 申请持久化读取权限
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
getContentResolver().takePersistableUriPermission(
|
||||
selectedImage,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
);
|
||||
LogUtils.d(TAG, "【选图权限】已添加持久化权限");
|
||||
}
|
||||
|
||||
// 2. 强制拷贝到应用私有目录(彻底规避系统虚拟路径问题)
|
||||
// String privateImagePath = copyUriToPrivateDir(selectedImage);
|
||||
// if (TextUtils.isEmpty(privateImagePath)) {
|
||||
// ToastUtils.show("系统图片路径无效,拷贝失败");
|
||||
// LogUtils.e(TAG, "【选图校验】系统Uri拷贝失败,Uri=" + selectedImage.toString());
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 3. 严格校验私有目录文件有效性
|
||||
// File srcFile = new File(privateImagePath);
|
||||
// if (srcFile == null
|
||||
// || !srcFile.exists()
|
||||
// || !srcFile.isFile()
|
||||
// || srcFile.length() <= 0) {
|
||||
// ToastUtils.show("系统图片文件无效");
|
||||
// LogUtils.e(TAG, "【选图校验】私有目录文件无效,路径=" + privateImagePath);
|
||||
// // 清理无效文件
|
||||
// if (srcFile != null && srcFile.exists()) {
|
||||
// srcFile.delete();
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
//LogUtils.d(TAG, "【选图校验】系统图片校验通过,私有路径:" + privateImagePath);
|
||||
|
||||
// 4. 同步到预览 Bean 并启动裁剪
|
||||
if (putUriFileToPreviewSource(selectedImage)) {
|
||||
LogUtils.d(TAG, "【选图同步】路径绑定完成");
|
||||
mBitmapCache.removeCachedBitmap(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundFilePath());
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
// 2. 同步到预览 Bean 并启动裁剪
|
||||
if (putUriFileToPreviewSource(selectedImage)) {
|
||||
LogUtils.d(TAG, "【选图同步】路径绑定完成");
|
||||
mBitmapCache.removeCachedBitmap(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundFilePath());
|
||||
ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this,
|
||||
mBgSourceUtils.getPreviewBackgroundBean(),
|
||||
mBackgroundView.getWidth(),
|
||||
mBackgroundView.getHeight(),
|
||||
false,
|
||||
REQUEST_CROP_IMAGE
|
||||
);
|
||||
} else {
|
||||
ToastUtils.show("图片同步失败");
|
||||
LogUtils.e(TAG, "【选图同步】文件复制失败");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ToastUtils.show("图片同步失败");
|
||||
LogUtils.e(TAG, "【选图同步】文件复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
boolean putUriFileToPreviewSource(Uri srcUriFile) {
|
||||
File srcFile = new File(UriUtil.getFilePathFromUri(this, srcUriFile));
|
||||
String filePath = UriUtil.getFilePathFromUri(this, srcUriFile);
|
||||
// 关键修复:校验路径是否为空
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
LogUtils.e(TAG, "putUriFileToPreviewSource: Uri解析路径为空");
|
||||
return false;
|
||||
}
|
||||
File srcFile = new File(filePath);
|
||||
return putUriFileToPreviewSource(srcFile);
|
||||
}
|
||||
|
||||
boolean putUriFileToPreviewSource(File srcFile) {
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource(File srcFile) srcFile %s", srcFile));
|
||||
LogUtils.d(TAG, String.format("putUriFileToPreviewSource(File srcFile) srcFile %s", srcFile));
|
||||
mBgSourceUtils.loadSettings();
|
||||
File dstFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundFilePath());
|
||||
return FileUtils.copyFile(srcFile, dstFile);
|
||||
@@ -588,16 +567,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
|
||||
if (systemFileRatio > 0) {
|
||||
Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
|
||||
if (cropBitmap != null && !cropBitmap.isRecycled()) {
|
||||
// 校验解析后的Bitmap是否有效
|
||||
if (isBitmapValid(cropBitmap)) {
|
||||
Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
|
||||
saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
|
||||
|
||||
if (scaledCropBitmap != cropBitmap && !scaledCropBitmap.isRecycled()) {
|
||||
// 仅当新Bitmap生成成功时,才保存并回收
|
||||
if (isBitmapValid(scaledCropBitmap)) {
|
||||
saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
|
||||
scaledCropBitmap.recycle();
|
||||
}
|
||||
if (!cropBitmap.isRecycled()) {
|
||||
cropBitmap.recycle();
|
||||
}
|
||||
cropBitmap.recycle();
|
||||
} else {
|
||||
LogUtils.e(TAG, "handleCropImageResult: 裁剪Bitmap解析无效");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,11 +618,21 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
return systemRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Bitmap 是否有效(未被回收且不为空)
|
||||
* @param bitmap 待校验的 Bitmap
|
||||
* @return true=有效 false=无效
|
||||
*/
|
||||
private boolean isBitmapValid(Bitmap bitmap) {
|
||||
return bitmap != null && !bitmap.isRecycled();
|
||||
}
|
||||
|
||||
private Bitmap adjustBitmapToFinalRatio(Bitmap originalBitmap, float finalCropRatio) {
|
||||
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整比例");
|
||||
if (originalBitmap == null || originalBitmap.isRecycled() || finalCropRatio <= 0) {
|
||||
// 增加 Bitmap 有效性校验
|
||||
if (!isBitmapValid(originalBitmap) || finalCropRatio <= 0) {
|
||||
LogUtils.e(TAG, "adjustBitmapToFinalRatio: 参数无效");
|
||||
return originalBitmap;
|
||||
return null; // 直接返回null,避免传递无效Bitmap
|
||||
}
|
||||
|
||||
int originalWidth = originalBitmap.getWidth();
|
||||
@@ -650,8 +640,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
float originalRatio = (float) originalWidth / originalHeight;
|
||||
|
||||
if (Math.abs(originalRatio - finalCropRatio) < 0.001f) {
|
||||
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 比例一致");
|
||||
return originalBitmap;
|
||||
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 比例一致,生成副本");
|
||||
// 关键修复:不返回原Bitmap,而是生成副本,避免原Bitmap被回收后失效
|
||||
return originalBitmap.copy(originalBitmap.getConfig(), false);
|
||||
}
|
||||
|
||||
int targetWidth, targetHeight;
|
||||
@@ -665,20 +656,24 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
targetWidth = Math.round(targetHeight * finalCropRatio);
|
||||
LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight);
|
||||
|
||||
Bitmap adjustedBitmap = Bitmap.createBitmap(
|
||||
originalBitmap,
|
||||
(originalWidth - targetWidth) / 2,
|
||||
(originalHeight - targetHeight) / 2,
|
||||
targetWidth,
|
||||
targetHeight
|
||||
);
|
||||
|
||||
return adjustedBitmap;
|
||||
try {
|
||||
Bitmap adjustedBitmap = Bitmap.createBitmap(
|
||||
originalBitmap,
|
||||
(originalWidth - targetWidth) / 2,
|
||||
(originalHeight - targetHeight) / 2,
|
||||
targetWidth,
|
||||
targetHeight
|
||||
);
|
||||
return adjustedBitmap;
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "adjustBitmapToFinalRatio: OOM异常");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) {
|
||||
LogUtils.d(TAG, "saveScaledBitmapToFile: 保存图片");
|
||||
if (bitmap == null || bitmap.isRecycled() || targetFile == null) {
|
||||
if (!isBitmapValid(bitmap) || targetFile == null) {
|
||||
LogUtils.e(TAG, "saveScaledBitmapToFile: 参数无效");
|
||||
return;
|
||||
}
|
||||
@@ -735,7 +730,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
Bitmap cropBitmap = null;
|
||||
try {
|
||||
cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options);
|
||||
if (cropBitmap == null || cropBitmap.isRecycled()) {
|
||||
if (!isBitmapValid(cropBitmap)) {
|
||||
LogUtils.e(TAG, "parseCropTempFileToBitmap: 解析失败");
|
||||
return null;
|
||||
}
|
||||
@@ -754,38 +749,29 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
|
||||
private void doubleRefreshPreview() {
|
||||
LogUtils.d(TAG, "【双重刷新】开始");
|
||||
// 清空缓存工具类的旧数据
|
||||
if (mBgSourceUtils != null) {
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean != null && previewBean.isUseBackgroundFile()) {
|
||||
String bgPath = previewBean.isUseBackgroundScaledCompressFile()
|
||||
? previewBean.getBackgroundScaledCompressFilePath()
|
||||
: previewBean.getBackgroundFilePath();
|
||||
mBitmapCache.removeCachedBitmap(bgPath);
|
||||
LogUtils.d(TAG, "【双重刷新】已清空工具类缓存:" + bgPath);
|
||||
}
|
||||
// 增加非空校验
|
||||
if (mBgSourceUtils == null || mBackgroundView == null || isFinishing()) {
|
||||
LogUtils.w(TAG, "【双重刷新】跳过:对象为空或Activity已结束");
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空 BackgroundView 自身的 Bitmap 引用
|
||||
// if (mBackgroundView != null) {
|
||||
// if (mBackgroundView instanceof ImageView) {
|
||||
// ((ImageView) mBackgroundView).setImageBitmap(null);
|
||||
// }
|
||||
// Drawable drawable = mBackgroundView.getBackground();
|
||||
// if (drawable != null) {
|
||||
// drawable.setCallback(null);
|
||||
// mBackgroundView.setBackground(null);
|
||||
// }
|
||||
// LogUtils.d(TAG, "【双重刷新】已清空控件 Bitmap 引用");
|
||||
// }
|
||||
// 清空缓存工具类的旧数据
|
||||
BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean();
|
||||
if (previewBean != null && previewBean.isUseBackgroundFile()) {
|
||||
String bgPath = previewBean.isUseBackgroundScaledCompressFile()
|
||||
? previewBean.getBackgroundScaledCompressFilePath()
|
||||
: previewBean.getBackgroundFilePath();
|
||||
mBitmapCache.removeCachedBitmap(bgPath);
|
||||
LogUtils.d(TAG, "【双重刷新】已清空工具类缓存:" + bgPath);
|
||||
}
|
||||
|
||||
// 重新加载最新数据
|
||||
if (mBackgroundView != null && !isFinishing()) {
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
|
||||
LogUtils.d(TAG, "【双重刷新】第一重完成");
|
||||
} else {
|
||||
LogUtils.w(TAG, "【双重刷新】跳过");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -793,10 +779,14 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mBackgroundView != null && !isFinishing()) {
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
if (mBackgroundView != null && !isFinishing() && mBgSourceUtils != null) {
|
||||
try {
|
||||
mBgSourceUtils.loadSettings();
|
||||
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
|
||||
LogUtils.d(TAG, "【双重刷新】第二重完成");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
@@ -863,62 +853,57 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
||||
|
||||
/**
|
||||
* 解析Uri到真实文件路径,兼容所有Android版本
|
||||
* @param uri 图片的Content Uri
|
||||
* @return 真实文件路径 / null
|
||||
*/
|
||||
// private String getRealPathFromUri(Uri uri) {
|
||||
// String path = null;
|
||||
// String[] projection = {MediaStore.Images.Media.DATA};
|
||||
// Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
|
||||
// if (cursor != null) {
|
||||
// if (cursor.moveToFirst()) {
|
||||
// int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||
// path = cursor.getString(columnIndex);
|
||||
// }
|
||||
// cursor.close();
|
||||
// }
|
||||
// // 针对Android 10+沙箱限制,拷贝到私有目录
|
||||
// if (TextUtils.isEmpty(path)) {
|
||||
//
|
||||
// path = copyUriToPrivateDir(uri);
|
||||
// }
|
||||
// return path;
|
||||
// }
|
||||
private String getRealPathFromUri(Uri uri) {
|
||||
String path = null;
|
||||
String[] projection = {MediaStore.Images.Media.DATA};
|
||||
Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
|
||||
path = cursor.getString(columnIndex);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
// 针对Android 10+沙箱限制,拷贝到私有目录
|
||||
if (TextUtils.isEmpty(path)) {
|
||||
path = copyUriToPrivateDir(uri);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Uri对应的图片拷贝到应用私有临时目录
|
||||
* @param uri 图片Uri
|
||||
* @return 拷贝后的文件路径 / null
|
||||
*/
|
||||
// private String copyUriToPrivateDir(Uri uri) {
|
||||
// try {
|
||||
// InputStream inputStream = getContentResolver().openInputStream(uri);
|
||||
// if (inputStream == null) {
|
||||
// return null;
|
||||
// }
|
||||
// File tempDir = new File(App.getTempDirPath());
|
||||
// if (!tempDir.exists()) {
|
||||
// tempDir.mkdirs();
|
||||
// }
|
||||
// String fileName = "select_temp_" + System.currentTimeMillis() + ".jpg";
|
||||
// File destFile = new File(tempDir, fileName);
|
||||
//
|
||||
// OutputStream outputStream = new FileOutputStream(destFile);
|
||||
// byte[] buffer = new byte[1024 * 4];
|
||||
// int len;
|
||||
// while ((len = inputStream.read(buffer)) != -1) {
|
||||
// outputStream.write(buffer, 0, len);
|
||||
// }
|
||||
// outputStream.flush();
|
||||
// inputStream.close();
|
||||
// outputStream.close();
|
||||
//
|
||||
// LogUtils.d(TAG, "【选图拷贝】文件已拷贝到私有目录:" + destFile.getAbsolutePath());
|
||||
// return destFile.getAbsolutePath();
|
||||
// } catch (IOException e) {
|
||||
// LogUtils.e(TAG, "【选图拷贝】异常:" + e.getMessage());
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
private String copyUriToPrivateDir(Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = getContentResolver().openInputStream(uri);
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
File tempDir = new File(App.getTempDirPath());
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs();
|
||||
}
|
||||
String fileName = "select_temp_" + System.currentTimeMillis() + ".jpg";
|
||||
File destFile = new File(tempDir, fileName);
|
||||
|
||||
OutputStream outputStream = new FileOutputStream(destFile);
|
||||
byte[] buffer = new byte[1024 * 4];
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, len);
|
||||
}
|
||||
outputStream.flush();
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
|
||||
LogUtils.d(TAG, "【选图拷贝】文件已拷贝到私有目录:" + destFile.getAbsolutePath());
|
||||
return destFile.getAbsolutePath();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "【选图拷贝】异常:" + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 09:14
|
||||
* @Describe Assets 目录拷贝工具类
|
||||
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
|
||||
*/
|
||||
public class AssetsCopyUtils {
|
||||
public static final String TAG = "AssetsCopyUtils";
|
||||
private static final int BUFFER_SIZE = 1024 * 8;
|
||||
|
||||
/**
|
||||
* 拷贝 assets/images/ 目录到指定目标目录
|
||||
* @param context 上下文
|
||||
* @param targetDirPath 目标目录完整路径(如 /sdcard/PowerBell/assets_images)
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
|
||||
// 拷贝 assets/images 根目录
|
||||
return copyAssetsDirToDir(context, "images", targetDirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归拷贝 assets 下指定目录到目标目录
|
||||
* @param context 上下文
|
||||
* @param assetsDir assets 下的源目录(如 "images"、"images/subdir")
|
||||
* @param targetDirPath 目标目录完整路径
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) {
|
||||
File targetDir = new File(targetDirPath);
|
||||
// 创建目标目录(含多级父目录)
|
||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||
Log.e(TAG, "创建目标目录失败:" + targetDirPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取 assets 目录下的文件/子目录列表
|
||||
String[] fileList = context.getAssets().list(assetsDir);
|
||||
if (fileList == null || fileList.length == 0) {
|
||||
Log.d(TAG, "assets 目录为空:" + assetsDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String fileName : fileList) {
|
||||
String assetsFilePath = assetsDir + File.separator + fileName;
|
||||
String targetFilePath = targetDirPath + File.separator + fileName;
|
||||
|
||||
// 判断当前项是文件还是子目录
|
||||
String[] subFileList = context.getAssets().list(assetsFilePath);
|
||||
if (subFileList != null && subFileList.length > 0) {
|
||||
// 是子目录,递归拷贝
|
||||
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 是文件,直接拷贝
|
||||
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝 assets 下单个文件到指定路径
|
||||
* @param context 上下文
|
||||
* @param assetsFilePath assets 下的文件路径(如 "images/cloud.png")
|
||||
* @param targetFilePath 目标文件完整路径
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) {
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetsFilePath);
|
||||
File targetFile = new File(targetFilePath);
|
||||
// 覆盖已存在的文件
|
||||
if (targetFile.exists() && !targetFile.delete()) {
|
||||
Log.w(TAG, "覆盖目标文件失败,跳过:" + targetFilePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
outputStream = new FileOutputStream(targetFile);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭流
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
if (outputStream != null) outputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "关闭流异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,75 +246,97 @@ public class BackgroundSourceUtils {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
boolean checkEmptyBackgroundAndCreateBlankBackgroundBean(BackgroundBean checkBackgroundBean) {
|
||||
File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath());
|
||||
if (!fCheckBackgroundFile.exists()) {
|
||||
String newCropFileName = "blank10x10";
|
||||
String fileSuffix = "png";
|
||||
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
|
||||
|
||||
AssetsCopyUtils.copyAssetsFileToDir(mContext, "images/blank10x10.png", mCropSourceFile.getAbsolutePath());
|
||||
try {
|
||||
mCropResultFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
// 加载图片数据模型数据
|
||||
loadSettings();
|
||||
// 修改预览数据模型
|
||||
previewBackgroundBean.setIsUseBackgroundFile(true);
|
||||
previewBackgroundBean.setIsUseBackgroundScaledCompressFile(false);
|
||||
|
||||
previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName());
|
||||
previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath());
|
||||
|
||||
previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName());
|
||||
previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath());
|
||||
// 保存数据模型数据更改
|
||||
saveSettings();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* 创建预览数据剪裁环境
|
||||
*/
|
||||
public boolean createAndUpdatePreviewEnvironmentForCropping(boolean isCreateNewPreviewBean, BackgroundBean previewBackgroundBean) {
|
||||
public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) {
|
||||
InputStream is = null;
|
||||
FileOutputStream fos = null;
|
||||
|
||||
try {
|
||||
clearCropTempFiles();
|
||||
if(checkEmptyBackgroundAndCreateBlankBackgroundBean(oldPreviewBackgroundBean)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Uri uri = UriUtil.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
|
||||
//String szType = mContext.getContentResolver().getType(uri);
|
||||
// 2. 截取MIME类型后缀(如从image/jpeg中提取jpeg)【核心新增逻辑】
|
||||
String fileSuffix = FileUtils.getFileSuffix(mContext, uri);
|
||||
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
|
||||
String fileSuffix;
|
||||
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
|
||||
|
||||
if (isCreateNewPreviewBean) {
|
||||
fileSuffix = "png";
|
||||
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
|
||||
DrawableToFileUtils.saveDrawableToFile(mContext, R.drawable.blank10x10, mCropSourceFile.getAbsolutePath());
|
||||
mCropResultFile.createNewFile();
|
||||
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) {
|
||||
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile);
|
||||
} else {
|
||||
if (FileUtils.isFileExists(previewBackgroundBean.getBackgroundFilePath())) {
|
||||
File fSourceFileOld = new File(previewBackgroundBean.getBackgroundFilePath());
|
||||
Uri uri = UriUtil.getUriForFile(mContext, fSourceFileOld);
|
||||
fileSuffix = FileUtils.getFileSuffix(mContext, uri);
|
||||
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
|
||||
mCropResultFile.createNewFile();
|
||||
}
|
||||
|
||||
FileUtils.copyFile(new File(previewBackgroundBean.getBackgroundFilePath()), mCropSourceFile);
|
||||
if (FileUtils.isFileExists(previewBackgroundBean.getBackgroundScaledCompressFilePath())) {
|
||||
FileUtils.copyFile(new File(previewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile);
|
||||
} else {
|
||||
mCropResultFile.createNewFile();
|
||||
}
|
||||
|
||||
mCropSourceFile.createNewFile();
|
||||
// 1. 打开Uri输入流(兼容content:///file:// 等多种Uri格式)
|
||||
is = mContext.getContentResolver().openInputStream(uri);
|
||||
if (is == null) {
|
||||
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败,Uri:" + uri.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 初始化选图临时文件输出流(Java7 手动创建流,不依赖try-with-resources)
|
||||
fos = new FileOutputStream(mCropSourceFile);
|
||||
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,平衡读写性能与内存占用
|
||||
int readLen; // 每次读取的字节长度
|
||||
|
||||
// 3. 流复制(Java7 标准while循环,避免Java8+语法)
|
||||
while ((readLen = is.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, readLen); // 精准写入读取到的字节,避免空字节填充
|
||||
}
|
||||
|
||||
// 4. 强制同步写入磁盘(解决Android 10+ 异步写入导致的文件无效问题)
|
||||
fos.flush();
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.getFD().sync(); // 确保数据写入物理磁盘,而非缓存
|
||||
} catch (IOException e) {
|
||||
LogUtils.w(TAG, "【选图解析】文件同步到磁盘失败,用flush()兜底:" + e.getMessage());
|
||||
fos.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// 解析成功日志(打印文件信息,便于问题排查)
|
||||
LogUtils.d(TAG, "【选图解析】Uri解析成功!");
|
||||
LogUtils.d(TAG, "→ 原Uri:" + uri.toString());
|
||||
} else {
|
||||
return createAndUpdatePreviewEnvironmentForCropping(true, previewBackgroundBean);
|
||||
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundFilePath())) {
|
||||
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundFilePath()), mCropSourceFile);
|
||||
} else {
|
||||
mCropSourceFile.createNewFile();
|
||||
// 1. 打开Uri输入流(兼容content:///file:// 等多种Uri格式)
|
||||
is = mContext.getContentResolver().openInputStream(uri);
|
||||
if (is == null) {
|
||||
LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败,Uri:" + uri.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 初始化选图临时文件输出流(Java7 手动创建流,不依赖try-with-resources)
|
||||
fos = new FileOutputStream(mCropSourceFile);
|
||||
byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,平衡读写性能与内存占用
|
||||
int readLen; // 每次读取的字节长度
|
||||
|
||||
// 3. 流复制(Java7 标准while循环,避免Java8+语法)
|
||||
while ((readLen = is.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, readLen); // 精准写入读取到的字节,避免空字节填充
|
||||
}
|
||||
|
||||
// 4. 强制同步写入磁盘(解决Android 10+ 异步写入导致的文件无效问题)
|
||||
fos.flush();
|
||||
if (fos != null) {
|
||||
try {
|
||||
fos.getFD().sync(); // 确保数据写入物理磁盘,而非缓存
|
||||
} catch (IOException e) {
|
||||
LogUtils.w(TAG, "【选图解析】文件同步到磁盘失败,用flush()兜底:" + e.getMessage());
|
||||
fos.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载图片数据模型数据
|
||||
@@ -328,12 +350,13 @@ public class BackgroundSourceUtils {
|
||||
// 保存数据模型数据更改
|
||||
saveSettings();
|
||||
|
||||
// 解析成功日志(打印文件信息,便于问题排查)
|
||||
// 6. 解析成功日志(打印文件信息,便于问题排查)
|
||||
LogUtils.d(TAG, "【选图解析】Uri解析成功!");
|
||||
LogUtils.d(TAG, "→ 原Uri:" + uri.toString());
|
||||
LogUtils.d(TAG, "→ 图片剪裁数据源:" + mCropSourceFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "→ 图片剪裁数据源文件大小:" + mCropSourceFile.length() + " bytes");
|
||||
LogUtils.d(TAG, "→ 剪裁结果数据文件:" + mCropResultFile.getAbsolutePath());
|
||||
LogUtils.d(TAG, "→ 剪裁结果数据文件大小:" + mCropResultFile.length() + " bytes");
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -477,6 +500,10 @@ public class BackgroundSourceUtils {
|
||||
return fBackgroundCompressDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getCropCacheDir() {
|
||||
return fCropCacheDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
public String getFileProviderAuthority() {
|
||||
return FILE_PROVIDER_AUTHORITY;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
* 特点:1. 单例模式 2. 压缩加载避免OOM 3. 路径- Bitmap 映射 4. 线程安全
|
||||
*/
|
||||
public class BitmapCacheUtils {
|
||||
private static final String TAG = "BitmapCacheUtils";
|
||||
public static final String TAG = "BitmapCacheUtils";
|
||||
// 最大图片尺寸(适配1080P屏幕,可根据需求调整)
|
||||
private static final int MAX_WIDTH = 1080;
|
||||
private static final int MAX_HEIGHT = 1920;
|
||||
|
||||
Reference in New Issue
Block a user