From 220aa9dbfb9167be2baf74531ac9fdee44dcb04d Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Thu, 11 Dec 2025 09:02:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E7=94=A8=E6=9D=83=E9=99=90=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E6=9B=B4=E6=96=B0=EF=BC=8C=E9=87=8D=E6=96=B0=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E5=89=AA=E8=A3=81=E6=96=87=E4=BB=B6=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- powerbell/build.properties | 4 +- .../java/cc/winboll/studio/powerbell/App.java | 1 - .../studio/powerbell/MainActivity.java | 16 +- .../BackgroundSettingsActivity.java | 489 +++++++++++------- .../activities/SettingsActivity.java | 14 +- .../utils/BackgroundSourceUtils.java | 151 +++--- .../powerbell/utils/PermissionUtils.java | 199 +++---- 7 files changed, 532 insertions(+), 342 deletions(-) diff --git a/powerbell/build.properties b/powerbell/build.properties index 97e9f6e..39e0ed4 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Dec 10 22:51:19 GMT 2025 +#Thu Dec 11 00:57:37 GMT 2025 stageCount=15 libraryProject= baseVersion=15.12 publishVersion=15.12.14 -buildCount=5 +buildCount=13 baseBetaVersion=15.12.15 diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java index d346524..f3ebe4f 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java @@ -44,7 +44,6 @@ public class App extends GlobalApplication { public void onCreate() { super.onCreate(); setIsDebugging(BuildConfig.DEBUG); - PermissionUtils.init(this); // 初始化活动窗口管理 WinBoLLActivityManager.init(this); diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java index 88e7400..f84ad81 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java @@ -58,7 +58,7 @@ public class MainActivity extends WinBoLLActivity { // ======================== 静态常量(移除缓存相关键)======================== public static final String TAG = "MainActivity"; - private static final int REQUEST_WRITE_STORAGE_PERMISSION = 1001; + private static final int REQUEST_READ_MEDIA_IMAGES = 1001; public static final int MSG_RELOAD_APPCONFIG = 0; public static final int MSG_CURRENTVALUEBATTERY = 1; public static final int MSG_LOAD_BACKGROUND = 2; @@ -123,10 +123,8 @@ public class MainActivity extends WinBoLLActivity { loadNonCriticalViewDelayed(); // 权限申请 - if (!PermissionUtils.getInstance().hasFullStoragePermission()) { - PermissionUtils.getInstance().requestFullStoragePermission(this); - } - } + PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES); + } // 移除 onSaveInstanceState 方法 // 移除 onRestoreInstanceState 方法 @@ -217,6 +215,14 @@ public class MainActivity extends WinBoLLActivity { } } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_READ_MEDIA_IMAGES) { + PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults); + } + } + @Override public void onBackPressed() { moveTaskToBack(true); diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java index 898252e..5e123e0 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -3,6 +3,7 @@ package cc.winboll.studio.powerbell.activities; import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; @@ -14,11 +15,24 @@ import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; +import android.text.TextUtils; import android.view.View; +import android.widget.ImageView; import android.widget.Toast; + import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; import androidx.core.content.FileProvider; +import androidx.appcompat.widget.Toolbar; + +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; + +import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.App; @@ -27,26 +41,27 @@ import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog; import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; +import cc.winboll.studio.powerbell.utils.BitmapCacheUtils; import cc.winboll.studio.powerbell.utils.FileUtils; 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 java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { public static final String TAG = "BackgroundSettingsActivity"; private BackgroundSourceUtils mBgSourceUtils; private PermissionUtils mPermissionUtils; + private BitmapCacheUtils mBitmapCache; + + // 新增:手动定义 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; public static final int REQUEST_CROP_IMAGE = 2; + private static final int REQUEST_READ_MEDIA = 1001; private Toolbar mToolbar; private BackgroundView mBackgroundView; @@ -73,6 +88,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mBgSourceUtils = BackgroundSourceUtils.getInstance(this); mBgSourceUtils.loadSettings(); mPermissionUtils = PermissionUtils.getInstance(); + mBitmapCache = BitmapCacheUtils.getInstance(); File tempDir = new File(App.getTempDirPath()); if (!tempDir.exists()) { @@ -95,9 +111,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mBgSourceUtils.setCurrentSourceToPreview(); } - Uri uriSelectedImage = UriUtil.getUriForFile(this, new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundFilePath())); - // 创建预览数据剪裁环境 - mBgSourceUtils.createAndUpdatePreviewEnvironmentForCropping(mBgSourceUtils.getPreviewBackgroundBean()); + //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()); doubleRefreshPreview(); LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成"); } @@ -109,18 +126,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } private void initToolbar() { - mToolbar = (Toolbar) findViewById(R.id.toolbar); + mToolbar = findViewById(R.id.toolbar); setSupportActionBar(mToolbar); mToolbar.setSubtitle(getTag()); mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText); getSupportActionBar().setDisplayHomeAsUpEnabled(true); mToolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【导航栏】点击返回"); - finish(); - } - }); + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【导航栏】点击返回"); + finish(); + } + }); } private void initClickListeners() { @@ -151,10 +168,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg boolean isImageType(String lowerMimeType) { return lowerMimeType.equals("image/jpeg") - || lowerMimeType.equals("image/png") - || lowerMimeType.equals("image/tiff") - || lowerMimeType.equals("image/jpg") - || lowerMimeType.equals("image/svg+xml"); + || lowerMimeType.equals("image/png") + || lowerMimeType.equals("image/tiff") + || lowerMimeType.equals("image/jpg") + || lowerMimeType.equals("image/svg+xml"); } @Override @@ -173,81 +190,98 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } }; - // ====================== 核心修改:Java 7 兼容的相册选择点击事件 ====================== private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "【按钮点击】选择图片"); - // 适配 API 30+ 权限逻辑 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // API 30+ 优先判断全文件管理权限 - if (!Environment.isExternalStorageManager()) { - mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this); - return; - } - } else { - // 低版本判断传统读写权限 - if (!mPermissionUtils.hasFullStoragePermission()) { - mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this); - return; - } - } + @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(); + } + } + } + }; - // API 30+ 推荐的标准化相册选择 Intent - Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - pickIntent.setType("image/*"); - // 添加权限 Flag,确保 Uri 可读取 - pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - // 持久化 Uri 权限,避免后续操作失效 - pickIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); - } + private void launchImageSelector() { + LogUtils.d(TAG, "【选图权限】已获取,启动选择器"); + Intent[] intents = new Intent[3]; + Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); + getContentIntent.setType("image/*"); + getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); + getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intents[0] = getContentIntent; - // 构建选择器,统一用户交互 - Intent chooser = Intent.createChooser(pickIntent, "选择图片"); - if (chooser.resolveActivity(getPackageManager()) != null) { - startActivityForResult(chooser, REQUEST_SELECT_PICTURE); - LogUtils.d(TAG, "【选图意图】启动图片选择"); - } else { - LogUtils.d(TAG, "【选图意图】无相册应用"); - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastUtils.show("未找到相册应用,请安装后重试"); - new AlertDialog.Builder(BackgroundSettingsActivity.this) - .setTitle("无图片选择应用") - .setMessage("需要安装相册应用才能选择图片") - .setPositiveButton("确定", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent marketIntent = new Intent(Intent.ACTION_VIEW); - marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); - if (marketIntent.resolveActivity(getPackageManager()) != null) { - startActivity(marketIntent); - } else { - ToastUtils.show("无法打开应用商店"); - } - } - }) - .setNegativeButton("取消", null) - .show(); - } - }); + Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + pickIntent.setType("image/*"); + pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intents[1] = pickIntent; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + openDocIntent.setType("image/*"); + openDocIntent.addCategory(Intent.CATEGORY_OPENABLE); + openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + intents[2] = openDocIntent; + } + + Intent validIntent = null; + for (int i = 0; i < intents.length; i++) { + Intent intent = intents[i]; + if (intent != null && intent.resolveActivity(getPackageManager()) != null) { + validIntent = intent; + break; } } - }; + + if (validIntent != null) { + Intent chooser = Intent.createChooser(validIntent, "选择图片"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(chooser, REQUEST_SELECT_PICTURE); + LogUtils.d(TAG, "【选图意图】启动图片选择"); + } else { + LogUtils.d(TAG, "【选图意图】无相册应用"); + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("未找到相册应用,请安装后重试"); + new AlertDialog.Builder(BackgroundSettingsActivity.this) + .setTitle("无图片选择应用") + .setMessage("需要安装相册应用才能选择图片") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent marketIntent = new Intent(Intent.ACTION_VIEW); + marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); + if (marketIntent.resolveActivity(getPackageManager()) != null) { + startActivity(marketIntent); + } else { + ToastUtils.show("无法打开应用商店"); + } + } + }) + .setNegativeButton("取消", null) + .show(); + } + }); + } + } private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】固定比例裁剪"); - // 调用裁剪工具类:传入上下文、预览图、固定比例(按视图宽高)、请求码 ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, - mBgSourceUtils.getPreviewBackgroundBean(), - mBackgroundView.getWidth(), - mBackgroundView.getHeight(), - false, - REQUEST_CROP_IMAGE); + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE + ); } }; @@ -255,13 +289,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】自由裁剪"); - // 调用裁剪工具类:传入上下文、预览图、自由裁剪(比例参数传0)、请求码 ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, - mBgSourceUtils.getPreviewBackgroundBean(), - 0, - 0, - true, - REQUEST_CROP_IMAGE); + mBgSourceUtils.getPreviewBackgroundBean(), + 0, + 0, + true, + REQUEST_CROP_IMAGE + ); } }; @@ -286,7 +320,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - if (mPermissionUtils.hasFullStoragePermission()) { + if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { LogUtils.d(TAG, "【拍照权限】已获取"); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { @@ -300,7 +334,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.e(TAG, "【拍照失败】"); } } else { - mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this); LogUtils.d(TAG, "【拍照权限】已申请"); } } @@ -318,7 +351,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void onClick(View v) { LogUtils.d(TAG, "【按钮点击】像素拾取"); - String targetImagePath = mBgSourceUtils.getCurrentBackgroundFilePath(); + String targetImagePath = mBgSourceUtils.getCurrentBackgroundBean().getBackgroundFilePath(); File targetFile = new File(targetImagePath); if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { ToastUtils.show("无有效图片可拾取像素"); @@ -352,7 +385,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode); try { - if (requestCode == PermissionUtils.REQUEST_CODE_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (requestCode == PermissionUtils.REQUEST_READ_MEDIA_IMAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { handleStoragePermissionCallback(); return; } @@ -393,8 +426,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } private void handleTakePhotoResult(int resultCode, Intent data) { - if (resultCode != RESULT_OK || !mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { + if (resultCode != RESULT_OK || data == null) { handleOperationCancelOrFail(); + return; + } + + if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { ToastUtils.show("拍照文件无效"); return; } @@ -410,13 +447,13 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath()); doubleRefreshPreview(); - // 拍照后启动固定比例裁剪(调用工具类) ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, - mBgSourceUtils.getPreviewBackgroundBean(), - mBackgroundView.getWidth(), - mBackgroundView.getHeight(), - false, - REQUEST_CROP_IMAGE); + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE + ); LogUtils.d(TAG, "【拍照完成】已启动裁剪"); } @@ -457,36 +494,68 @@ 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()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - 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, "【选图权限】已添加持久化权限"); + } - LogUtils.d(TAG, "【选图同步】路径绑定完成"); - // 选图后启动固定比例裁剪(调用工具类) - putUriFileToPreviewSource(selectedImage); - ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, - mBgSourceUtils.getPreviewBackgroundBean(), - mBackgroundView.getWidth(), - mBackgroundView.getHeight(), - false, - REQUEST_CROP_IMAGE); - } + // 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, + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE + ); + } else { + ToastUtils.show("图片同步失败"); + LogUtils.e(TAG, "【选图同步】文件复制失败"); + } + } boolean putUriFileToPreviewSource(Uri srcUriFile) { File srcFile = new File(UriUtil.getFilePathFromUri(this, srcUriFile)); @@ -494,6 +563,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } boolean putUriFileToPreviewSource(File 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); @@ -531,16 +601,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - // 延迟300ms调用双重刷新,确保文件写入完成 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - if (!isFinishing()) { - doubleRefreshPreview(); - LogUtils.d(TAG, "handleCropImageResult: 触发双重刷新"); - } - } - }, 300); + @Override + public void run() { + if (!isFinishing()) { + doubleRefreshPreview(); + LogUtils.d(TAG, "handleCropImageResult: 触发双重刷新"); + } + } + }, 300); } else { handleOperationCancelOrFail(); } @@ -597,11 +666,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight); Bitmap adjustedBitmap = Bitmap.createBitmap( - originalBitmap, - (originalWidth - targetWidth) / 2, - (originalHeight - targetHeight) / 2, - targetWidth, - targetHeight + originalBitmap, + (originalWidth - targetWidth) / 2, + (originalHeight - targetHeight) / 2, + targetWidth, + targetHeight ); return adjustedBitmap; @@ -685,20 +754,32 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg private void doubleRefreshPreview() { LogUtils.d(TAG, "【双重刷新】开始"); - // 1. 清空缓存工具类的旧数据 + // 清空缓存工具类的旧数据 if (mBgSourceUtils != null) { BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); if (previewBean != null && previewBean.isUseBackgroundFile()) { String bgPath = previewBean.isUseBackgroundScaledCompressFile() - ? previewBean.getBackgroundScaledCompressFilePath() - : previewBean.getBackgroundFilePath(); - // 强制清除缓存 - App._mBitmapCacheUtils.removeCachedBitmap(bgPath); + ? previewBean.getBackgroundScaledCompressFilePath() + : previewBean.getBackgroundFilePath(); + mBitmapCache.removeCachedBitmap(bgPath); LogUtils.d(TAG, "【双重刷新】已清空工具类缓存:" + bgPath); } } - // 2. 重新加载最新数据 + // 清空 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 引用"); +// } + + // 重新加载最新数据 if (mBackgroundView != null && !isFinishing()) { mBgSourceUtils.loadSettings(); mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); @@ -708,23 +789,23 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - // 3. 延长延迟到200ms,确保文件完全加载 + // 延长延迟到200ms,确保文件完全加载 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { - @Override - public void run() { - if (mBackgroundView != null && !isFinishing()) { - mBgSourceUtils.loadSettings(); - mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); - LogUtils.d(TAG, "【双重刷新】第二重完成"); - } - } - }, 200); + @Override + public void run() { + if (mBackgroundView != null && !isFinishing()) { + mBgSourceUtils.loadSettings(); + mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); + LogUtils.d(TAG, "【双重刷新】第二重完成"); + } + } + }, 200); } private void handleOperationCancelOrFail() { mBgSourceUtils.setCurrentSourceToPreview(); LogUtils.d(TAG, "【操作回调】取消或失败"); - ToastUtils.show("操作取消或失败"); + ToastUtils.show("【操作回调】取消或失败"); doubleRefreshPreview(); } @@ -752,30 +833,92 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg @Override public void finish() { if (isCommitSettings) { + setResult(RESULT_OK); super.finish(); } else { - // 如果预览背景改变过就提示是否更换背景 if (isPreviewBackgroundChanged) { YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() { - @Override - public void onYes() { - mBgSourceUtils.commitPreviewSourceToCurrent(); - isCommitSettings = true; - finish(); - } + @Override + public void onYes() { + mBgSourceUtils.commitPreviewSourceToCurrent(); + isCommitSettings = true; + setResult(RESULT_OK); + finish(); + } - @Override - public void onNo() { - isCommitSettings = true; - finish(); - } - }); + @Override + public void onNo() { + isCommitSettings = true; + setResult(RESULT_CANCELED); + finish(); + } + }); } else { - // 如果预览背景未改变就直接退出 + setResult(RESULT_OK); isCommitSettings = true; finish(); } } } + + /** + * 解析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; +// } + + /** + * 将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; +// } +// } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java index bc7b2ac..a8acf58 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java @@ -18,6 +18,7 @@ import cc.winboll.studio.powerbell.utils.PermissionUtils; public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity { public static final String TAG = "SettingsActivity"; + private static final int REQUEST_READ_MEDIA_IMAGES = 1001; private Toolbar mToolbar; @@ -52,10 +53,15 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit public void onCheckPermission(View view) { //ToastUtils.show("onCheckPermission"); - if (PermissionUtils.getInstance().hasFullStoragePermission()) { - ToastUtils.show("【权限检查】存储权限已全部获取"); - } else { - PermissionUtils.getInstance().requestFullStoragePermission(this); + PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES); + } + + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_READ_MEDIA_IMAGES) { + PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults); } } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java index f9a4508..4f48a19 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -4,12 +4,15 @@ import android.content.Context; import android.graphics.Bitmap; import android.media.ExifInterface; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.text.TextUtils; import android.util.Log; +import androidx.core.content.FileProvider; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.BuildConfig; +import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.model.BackgroundBean; import java.io.BufferedOutputStream; import java.io.File; @@ -19,8 +22,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; -import android.os.Build; -import androidx.core.content.FileProvider; /** * @Author ZhanGSKen @@ -248,57 +249,72 @@ public class BackgroundSourceUtils { /* * 创建预览数据剪裁环境 */ - public boolean createAndUpdatePreviewEnvironmentForCropping(BackgroundBean oldPreviewBackgroundBean) { + public boolean createAndUpdatePreviewEnvironmentForCropping(boolean isCreateNewPreviewBean, BackgroundBean previewBackgroundBean) { InputStream is = null; FileOutputStream fos = null; try { clearCropTempFiles(); - 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(); - mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); - mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); + String fileSuffix; - if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) { - FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile); - } else { + 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.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; - } + 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); - // 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(); + 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); } + } // 加载图片数据模型数据 @@ -312,13 +328,12 @@ 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) { @@ -380,30 +395,30 @@ public class BackgroundSourceUtils { /** * 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) */ - public String getCurrentBackgroundFilePath() { - String fileName = currentBackgroundBean.getBackgroundFileName(); - if (TextUtils.isEmpty(fileName)) { - LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径"); - return ""; - } - File file = new File(fBackgroundSourceDir, fileName); - LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath()); - return file.getAbsolutePath(); - } - - /** - * 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) - */ - public String getPreviewBackgroundFilePath() { - String fileName = previewBackgroundBean.getBackgroundFileName(); - if (TextUtils.isEmpty(fileName)) { - LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径"); - return ""; - } - File file = new File(fBackgroundSourceDir, fileName); - LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath()); - return file.getAbsolutePath(); - } +// public String getCurrentBackgroundFilePath() { +// String fileName = currentBackgroundBean.getBackgroundFileName(); +// if (TextUtils.isEmpty(fileName)) { +// LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径"); +// return ""; +// } +// File file = new File(fBackgroundSourceDir, fileName); +// LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath()); +// return file.getAbsolutePath(); +// } +// +// /** +// * 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) +// */ +// public String getPreviewBackgroundFilePath() { +// String fileName = previewBackgroundBean.getBackgroundFileName(); +// if (TextUtils.isEmpty(fileName)) { +// LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径"); +// return ""; +// } +// File file = new File(fBackgroundSourceDir, fileName); +// LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath()); +// return file.getAbsolutePath(); +// } /** * 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验,统一指向BackgroundCrops目录) diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java index f860c8f..924e0c9 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java @@ -1,113 +1,134 @@ package cc.winboll.studio.powerbell.utils; -import android.content.Context; -import android.content.Intent; +import android.app.Activity; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; -import android.os.Environment; -import android.provider.Settings; -import android.widget.Toast; + +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import cc.winboll.studio.libappbase.LogUtils; + /** - * 存储权限工具类(适配 API 30+) - * 核心支持:1. 传统读写权限 2. MANAGE_EXTERNAL_STORAGE 全文件权限 + * 权限申请工具类 + * 适配 Android 13+ 媒体权限 & 低版本存储权限 + * 兼容 SDK 版本低于 33 的编译环境 */ public class PermissionUtils { + private static final String TAG = "PermissionUtils"; + // 存储权限请求码 + public static final int REQUEST_STORAGE = 1000; + // 媒体图片权限请求码(Android 13+) + public static final int REQUEST_READ_MEDIA_IMAGES = 1001; + + // 手动定义 Android 13+ 媒体图片权限常量(解决 SDK 低于 33 无法识别问题) + private static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES"; + // Android 13 对应的 SDK 版本号(替代 Build.VERSION_CODES.TIRAMISU) + private static final int SDK_VERSION_TIRAMISU = 33; + + // 单例模式 private static volatile PermissionUtils sInstance; - private Context mContext; - // 权限请求码 - public static final int REQUEST_CODE_STORAGE = 1001; - public static final int REQUEST_CODE_MANAGE_STORAGE = 1002; - - private PermissionUtils(Context context) { - this.mContext = context.getApplicationContext(); - } + private PermissionUtils() {} public static PermissionUtils getInstance() { if (sInstance == null) { - throw new IllegalStateException("请先调用 init(Context) 初始化"); + synchronized (PermissionUtils.class) { + if (sInstance == null) { + sInstance = new PermissionUtils(); + } + } } return sInstance; } - public static void init(Context context) { - if (sInstance == null) { - synchronized (PermissionUtils.class) { - if (sInstance == null) { - sInstance = new PermissionUtils(context); + /** + * 检查并请求 存储权限(Android 12及以下) + */ + public boolean checkAndRequestStoragePermission(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Android 11+ 无需申请 READ_EXTERNAL_STORAGE,直接返回true + return true; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + String[] permissions = { + android.Manifest.permission.READ_EXTERNAL_STORAGE, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + if (ContextCompat.checkSelfPermission(activity, permissions[0]) != PackageManager.PERMISSION_GRANTED + || ContextCompat.checkSelfPermission(activity, permissions[1]) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(activity, permissions, REQUEST_STORAGE); + return false; + } + } + return true; + } + + /** + * 检查并请求 媒体图片权限(Android 13+) + * 兼容 SDK 编译版本低于 33 的情况 + */ + public boolean checkAndRequestMediaImagesPermission(Activity activity, int requestCode) { + // 用数值 33 替代 Build.VERSION_CODES.TIRAMISU + if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) { + // 用手动定义的权限常量替代 android.Manifest.permission.READ_MEDIA_IMAGES + if (ContextCompat.checkSelfPermission(activity, READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(activity, new String[]{READ_MEDIA_IMAGES}, requestCode); + return false; + } + } + // 低版本已通过存储权限覆盖,直接返回true + return true; + } + + /** + * 权限请求结果处理 + */ + public void handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) { + switch (requestCode) { + case REQUEST_STORAGE: + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + LogUtils.d(TAG, "存储权限申请成功"); + } else { + LogUtils.e(TAG, "存储权限申请失败"); } + break; + case REQUEST_READ_MEDIA_IMAGES: + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + LogUtils.d(TAG, "媒体图片权限申请成功"); + } else { + LogUtils.e(TAG, "媒体图片权限申请失败"); + } + break; + default: + LogUtils.d(TAG, "未知权限请求码:" + requestCode); + } + } + + /** + * 检查是否有管理所有文件权限(Android 11+) + */ + public boolean checkManageExternalStoragePermission(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return android.os.Environment.isExternalStorageManager(); + } + return true; + } + + /** + * 请求管理所有文件权限(Android 11+) + */ + public void requestManageExternalStoragePermission(Activity activity, int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + android.content.Intent intent = new android.content.Intent( + android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION + ); + intent.setData(android.net.Uri.parse("package:" + activity.getPackageName())); + activity.startActivityForResult(intent, requestCode); + } catch (Exception e) { + LogUtils.e(TAG, "请求管理文件权限异常:" + e.getMessage()); } } } - - /** - * 检查是否拥有完整的文件管理权限 - */ - public boolean hasFullStoragePermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // API 30+ 需判断 MANAGE_EXTERNAL_STORAGE 权限 - return Environment.isExternalStorageManager(); - } else { - // 低版本判断传统读写权限 - int read = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.READ_EXTERNAL_STORAGE); - int write = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.WRITE_EXTERNAL_STORAGE); - return read == PackageManager.PERMISSION_GRANTED && write == PackageManager.PERMISSION_GRANTED; - } - } - - /** - * 申请文件管理权限(自动适配系统版本) - * @param activity 发起申请的 Activity - */ - public void requestFullStoragePermission(android.app.Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // API 30+ 跳转系统设置页开启权限 - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); - Uri uri = Uri.fromParts("package", mContext.getPackageName(), null); - intent.setData(uri); - activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE); - } else { - // 低版本申请传统读写权限 - androidx.core.app.ActivityCompat.requestPermissions( - activity, - new String[]{ - android.Manifest.permission.READ_EXTERNAL_STORAGE, - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - }, - REQUEST_CODE_STORAGE - ); - } - } - - /** - * 处理低版本权限申请结果(在 Activity 的 onRequestPermissionsResult 中调用) - * @param activity 上下文 - * @param requestCode 请求码 - * @param permissions 权限数组 - * @param grantResults 授权结果 - * @return 权限是否申请成功 - */ - public boolean handleStoragePermissionResult(android.app.Activity activity, int requestCode, String[] permissions, int[] grantResults) { - // 仅处理传统存储权限的请求结果 - if (requestCode != REQUEST_CODE_STORAGE) { - return false; - } - // 校验授权结果:读写权限均需授予 - boolean isGranted = grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED - && grantResults[1] == PackageManager.PERMISSION_GRANTED; - - if (isGranted) { - // 权限授予成功,提示用户 - Toast.makeText(activity, "存储权限申请成功", Toast.LENGTH_SHORT).show(); - } else { - // 权限被拒绝,引导用户手动开启 - Toast.makeText(activity, "存储权限被拒绝,部分功能无法使用", Toast.LENGTH_SHORT).show(); - } - return isGranted; - } }