相册权限申请模块改进中。。。

This commit is contained in:
2025-12-11 06:53:22 +08:00
parent 6ed9bc0d8e
commit ecafd2026f
7 changed files with 311 additions and 380 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 方法

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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&豆包大模型<zhangsken@qq.com>
* @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<String> 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;
}
}

View File

@@ -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;