diff --git a/powerbell/build.properties b/powerbell/build.properties index b9c4173..97e9f6e 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Thu Dec 11 03:22:10 HKT 2025 +#Wed Dec 10 22:51:19 GMT 2025 stageCount=15 libraryProject= baseVersion=15.12 publishVersion=15.12.14 -buildCount=0 +buildCount=5 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 9b6900e..d346524 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java @@ -14,6 +14,7 @@ import cc.winboll.studio.powerbell.utils.AppCacheUtils; import cc.winboll.studio.powerbell.utils.AppConfigUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BitmapCacheUtils; +import cc.winboll.studio.powerbell.utils.PermissionUtils; import java.io.File; public class App extends GlobalApplication { @@ -43,6 +44,7 @@ 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 26bb071..88e7400 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java @@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell; import android.app.Activity; import android.app.Fragment; -import android.app.FragmentTransaction; import android.content.Intent; import android.database.Cursor; import android.graphics.drawable.Drawable; @@ -25,7 +24,6 @@ import android.widget.SeekBar; import android.widget.Switch; import android.widget.TextView; import androidx.appcompat.widget.Toolbar; - import cc.winboll.studio.libaes.activitys.AboutActivity; import cc.winboll.studio.libaes.models.APPInfo; import cc.winboll.studio.libaes.utils.AESThemeUtil; @@ -125,7 +123,9 @@ public class MainActivity extends WinBoLLActivity { loadNonCriticalViewDelayed(); // 权限申请 - PermissionUtils.getInstance().checkAndRequestStoragePermission(this); + if (!PermissionUtils.getInstance().hasFullStoragePermission()) { + PermissionUtils.getInstance().requestFullStoragePermission(this); + } } // 移除 onSaveInstanceState 方法 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 6134bad..898252e 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 @@ -15,11 +15,10 @@ import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; 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 cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.App; @@ -28,17 +27,16 @@ 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.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; -import androidx.appcompat.widget.Toolbar; -import cc.winboll.studio.powerbell.utils.UriUtil; -import cc.winboll.studio.powerbell.utils.FileUtils; public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { @@ -111,18 +109,18 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } private void initToolbar() { - mToolbar = findViewById(R.id.toolbar); + mToolbar = (Toolbar) 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() { @@ -153,10 +151,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 @@ -175,74 +173,66 @@ 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, "【按钮点击】选择图片"); - if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { - 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 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(); - } - }); + // 适配 API 30+ 权限逻辑 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // API 30+ 优先判断全文件管理权限 + if (!Environment.isExternalStorageManager()) { + mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this); + return; } } else { - LogUtils.d(TAG, "【选图权限】已申请"); + // 低版本判断传统读写权限 + if (!mPermissionUtils.hasFullStoragePermission()) { + mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this); + return; + } + } + + // 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); + } + + // 构建选择器,统一用户交互 + 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(); + } + }); } } }; @@ -253,11 +243,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg 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); } }; @@ -267,11 +257,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【按钮点击】自由裁剪"); // 调用裁剪工具类:传入上下文、预览图、自由裁剪(比例参数传0)、请求码 ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, - mBgSourceUtils.getPreviewBackgroundBean(), - 0, - 0, - true, - REQUEST_CROP_IMAGE); + mBgSourceUtils.getPreviewBackgroundBean(), + 0, + 0, + true, + REQUEST_CROP_IMAGE); } }; @@ -296,7 +286,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { + if (mPermissionUtils.hasFullStoragePermission()) { LogUtils.d(TAG, "【拍照权限】已获取"); Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { @@ -310,6 +300,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.e(TAG, "【拍照失败】"); } } else { + mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this); LogUtils.d(TAG, "【拍照权限】已申请"); } } @@ -361,7 +352,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode); try { - if (requestCode == PermissionUtils.REQUEST_MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (requestCode == PermissionUtils.REQUEST_CODE_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { handleStoragePermissionCallback(); return; } @@ -402,12 +393,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } private void handleTakePhotoResult(int resultCode, Intent data) { - if (resultCode != RESULT_OK || data == null) { + if (resultCode != RESULT_OK || !mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { handleOperationCancelOrFail(); - return; - } - - if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { ToastUtils.show("拍照文件无效"); return; } @@ -425,11 +412,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 拍照后启动固定比例裁剪(调用工具类) 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, "【拍照完成】已启动裁剪"); } @@ -484,8 +471,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { getContentResolver().takePersistableUriPermission( - selectedImage, - Intent.FLAG_GRANT_READ_URI_PERMISSION + selectedImage, + Intent.FLAG_GRANT_READ_URI_PERMISSION ); LogUtils.d(TAG, "【选图权限】已添加持久化权限"); } @@ -494,11 +481,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 选图后启动固定比例裁剪(调用工具类) putUriFileToPreviewSource(selectedImage); ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, - mBgSourceUtils.getPreviewBackgroundBean(), - mBackgroundView.getWidth(), - mBackgroundView.getHeight(), - false, - REQUEST_CROP_IMAGE); + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE); } boolean putUriFileToPreviewSource(Uri srcUriFile) { @@ -512,9 +499,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return FileUtils.copyFile(srcFile, dstFile); } - /** - * 核心修改:裁剪成功后调用双重刷新,确保最新图片加载 - */ private void handleCropImageResult(int requestCode, int resultCode, Intent data) { LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果"); File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath()); @@ -547,16 +531,16 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg } } - // 关键修改:延迟300ms调用双重刷新,确保文件写入完成 + // 延迟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(); } @@ -613,11 +597,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; @@ -699,9 +683,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return cropBitmap; } - /** - * 核心修改:添加缓存清理 + 控件 Bitmap 清空逻辑 - */ private void doubleRefreshPreview() { LogUtils.d(TAG, "【双重刷新】开始"); // 1. 清空缓存工具类的旧数据 @@ -709,30 +690,15 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); if (previewBean != null && previewBean.isUseBackgroundFile()) { String bgPath = previewBean.isUseBackgroundScaledCompressFile() - ? previewBean.getBackgroundScaledCompressFilePath() - : previewBean.getBackgroundFilePath(); + ? previewBean.getBackgroundScaledCompressFilePath() + : previewBean.getBackgroundFilePath(); // 强制清除缓存 App._mBitmapCacheUtils.removeCachedBitmap(bgPath); LogUtils.d(TAG, "【双重刷新】已清空工具类缓存:" + bgPath); } } - // 2. 清空 BackgroundView 自身的 Bitmap 引用 -// if (mBackgroundView != null) { -// // 清空 ImageView 持有的 Bitmap -// if (mBackgroundView instanceof BackgroundView) { -// ((BackgroundView) mBackgroundView).setImageBitmap(null); -// } -// // 清空背景 Drawable 引用 -// Drawable drawable = mBackgroundView.getBackground(); -// if (drawable != null) { -// drawable.setCallback(null); -// mBackgroundView.setBackground(null); -// } -// LogUtils.d(TAG, "【双重刷新】已清空控件 Bitmap 引用"); -// } - - // 3. 重新加载最新数据 + // 2. 重新加载最新数据 if (mBackgroundView != null && !isFinishing()) { mBgSourceUtils.loadSettings(); mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); @@ -742,35 +708,30 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg return; } - // 4. 延长延迟到200ms,确保文件完全加载 + // 3. 延长延迟到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(); } - /** - * 获取FileProvider Uri(复用方法,避免重复代码) - */ public Uri getFileProviderUri(File file) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider"; - return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file); } else { return Uri.fromFile(file); @@ -796,19 +757,19 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg // 如果预览背景改变过就提示是否更换背景 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; + finish(); + } - @Override - public void onNo() { - isCommitSettings = true; - finish(); - } - }); + @Override + public void onNo() { + isCommitSettings = true; + finish(); + } + }); } else { // 如果预览背景未改变就直接退出 isCommitSettings = true; 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 8693d94..bc7b2ac 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 @@ -52,8 +52,10 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit public void onCheckPermission(View view) { //ToastUtils.show("onCheckPermission"); - if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) { + if (PermissionUtils.getInstance().hasFullStoragePermission()) { ToastUtils.show("【权限检查】存储权限已全部获取"); + } else { + PermissionUtils.getInstance().requestFullStoragePermission(this); } } } 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 1c510fb..f860c8f 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,214 +1,113 @@ package cc.winboll.studio.powerbell.utils; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.provider.Settings; -import android.text.TextUtils; -import androidx.core.app.ActivityCompat; +import android.widget.Toast; 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&豆包大模型 - * @Date 2025/12/01 16:05 - * @Describe 权限申请工具类(单例) - * 核心特性: - * 1. 适配全Android版本(6.0+ 动态权限 / 13+ 兼容) - * 2. 支持多包名场景(无硬编码包名) - * 3. 统一权限校验、申请、回调处理 - * 4. 自带用户引导(拒绝权限+不再询问场景) + * 存储权限工具类(适配 API 30+) + * 核心支持:1. 传统读写权限 2. MANAGE_EXTERNAL_STORAGE 全文件权限 */ 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 Context mContext; - // 单例实例(双重校验锁,线程安全) - private static volatile PermissionUtils sInstance; + // 权限请求码 + public static final int REQUEST_CODE_STORAGE = 1001; + public static final int REQUEST_CODE_MANAGE_STORAGE = 1002; - // 私有构造(禁止外部实例化) - private PermissionUtils() {} + private PermissionUtils(Context context) { + this.mContext = context.getApplicationContext(); + } - /** - * 获取单例实例(适配多包名,无硬编码) - */ - public static PermissionUtils getInstance() { - if (sInstance == null) { - synchronized (PermissionUtils.class) { - if (sInstance == null) { - sInstance = new PermissionUtils(); - } - } - } - return sInstance; - } + public static PermissionUtils getInstance() { + if (sInstance == null) { + throw new IllegalStateException("请先调用 init(Context) 初始化"); + } + 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); + public static void init(Context context) { + if (sInstance == null) { + synchronized (PermissionUtils.class) { + if (sInstance == null) { + sInstance = new PermissionUtils(context); + } + } + } + } - // 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号) - String[] requiredPermissions = { - android.Manifest.permission.WRITE_EXTERNAL_STORAGE, - android.Manifest.permission.READ_EXTERNAL_STORAGE - }; + /** + * 检查是否拥有完整的文件管理权限 + */ + 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; + } + } - // 筛选未授予的权限 - List needPermissions = new ArrayList<>(); - for (String permission : requiredPermissions) { - if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { - needPermissions.add(permission); - } - } + /** + * 申请文件管理权限(自动适配系统版本) + * @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 + ); + } + } - // 无权限需要申请:触发动态申请 - 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; - } + /** + * 处理低版本权限申请结果(在 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; - // 所有权限已授予 - 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); - } - } + if (isGranted) { + // 权限授予成功,提示用户 + Toast.makeText(activity, "存储权限申请成功", Toast.LENGTH_SHORT).show(); + } else { + // 权限被拒绝,引导用户手动开启 + Toast.makeText(activity, "存储权限被拒绝,部分功能无法使用", Toast.LENGTH_SHORT).show(); + } + return isGranted; + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/UriUtil.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/UriUtil.java index 01060e1..43bb0a3 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/UriUtil.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/UriUtil.java @@ -10,8 +10,10 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.provider.MediaStore; import androidx.core.content.FileProvider; +import cc.winboll.studio.libappbase.LogUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -105,18 +107,83 @@ public class UriUtil { } public static Uri getUriForFile(Context context, String filePath) { + // 1. 打印传入的文件路径 + LogUtils.d(TAG, "getUriForFile -> 传入路径:" + filePath); + if (filePath == null || filePath.isEmpty()) { + LogUtils.e(TAG, "getUriForFile -> 传入路径为空"); + return null; + } + File file = new File(filePath); - return getUriForFile(context, file); - } + // 2. 打印File对象的绝对路径和存在性 + LogUtils.d(TAG, "getUriForFile -> 文件绝对路径:" + file.getAbsolutePath()); + LogUtils.d(TAG, "getUriForFile -> 文件是否存在:" + file.exists()); + LogUtils.d(TAG, "getUriForFile -> 是否为目录:" + file.isDirectory()); - public static Uri getUriForFile(Context context, File file) { - //Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file); - if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上 - return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file); - } - return Uri.fromFile(file); - } + // 3. 合法性校验 + if (!file.exists() || file.isDirectory()) { + LogUtils.e(TAG, "getUriForFile -> 非法路径:文件不存在或为目录"); + return null; + } + // 4. 校验路径是否在配置的合法目录内 + String appFilesDir = context.getExternalFilesDir(null) != null ? context.getExternalFilesDir(null).getAbsolutePath() : "null"; + String publicPicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBell/"; + String internalFilesDir = context.getFilesDir().getAbsolutePath(); + String cacheDir = context.getCacheDir().getAbsolutePath(); + + String absolutePath = file.getAbsolutePath(); + boolean isInConfigDir = absolutePath.startsWith(appFilesDir) + || absolutePath.startsWith(publicPicDir) + || absolutePath.startsWith(internalFilesDir) + || absolutePath.startsWith(cacheDir); + LogUtils.d(TAG, "getUriForFile -> 路径是否在配置目录内:" + isInConfigDir); + if (!isInConfigDir) { + LogUtils.w(TAG, "getUriForFile -> 路径不在FileProvider配置范围内,可能导致异常"); + // 非强制拦截,保留原有逻辑,仅警告 + } + + return getUriForFile(context, file); + } + + public static Uri getUriForFile(Context context, File file) { + if (context == null) { + LogUtils.e(TAG, "getUriForFile -> Context为空"); + return null; + } + if (file == null) { + LogUtils.e(TAG, "getUriForFile -> File对象为空"); + return null; + } + + // 1. 二次校验文件状态 + LogUtils.d(TAG, "getUriForFile(File) -> 文件路径:" + file.getAbsolutePath()); + if (!file.exists() || file.isDirectory()) { + LogUtils.e(TAG, "getUriForFile(File) -> 文件不存在或为目录"); + return null; + } + + // 2. 版本判断与Uri生成 + if (Build.VERSION.SDK_INT >= 24) { + LogUtils.d(TAG, "getUriForFile -> Android 7.0+,使用FileProvider生成Uri"); + try { + String authority = context.getPackageName() + ".fileprovider"; + LogUtils.d(TAG, "getUriForFile -> FileProvider authority:" + authority); + Uri uri = FileProvider.getUriForFile(context, authority, file); + LogUtils.d(TAG, "getUriForFile -> 生成Content Uri成功:" + uri.toString()); + return uri; + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "getUriForFile -> FileProvider生成Uri失败:路径未配置或权限不足", e); + return null; + } + } else { + LogUtils.d(TAG, "getUriForFile -> Android 7.0以下,使用Uri.fromFile生成Uri"); + Uri uri = Uri.fromFile(file); + LogUtils.d(TAG, "getUriForFile -> 生成File Uri成功:" + uri.toString()); + return uri; + } + } + private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) throws IOException { File targetFile = null;