相册权限申请模块改进中。。。
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#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
|
stageCount=15
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.12
|
baseVersion=15.12
|
||||||
publishVersion=15.12.14
|
publishVersion=15.12.14
|
||||||
buildCount=0
|
buildCount=5
|
||||||
baseBetaVersion=15.12.15
|
baseBetaVersion=15.12.15
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import cc.winboll.studio.powerbell.utils.AppCacheUtils;
|
|||||||
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
|
||||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
||||||
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
|
||||||
|
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
public class App extends GlobalApplication {
|
public class App extends GlobalApplication {
|
||||||
@@ -43,6 +44,7 @@ public class App extends GlobalApplication {
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
PermissionUtils.init(this);
|
||||||
|
|
||||||
// 初始化活动窗口管理
|
// 初始化活动窗口管理
|
||||||
WinBoLLActivityManager.init(this);
|
WinBoLLActivityManager.init(this);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.FragmentTransaction;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
@@ -25,7 +24,6 @@ import android.widget.SeekBar;
|
|||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
import cc.winboll.studio.libaes.activitys.AboutActivity;
|
||||||
import cc.winboll.studio.libaes.models.APPInfo;
|
import cc.winboll.studio.libaes.models.APPInfo;
|
||||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||||
@@ -125,7 +123,9 @@ public class MainActivity extends WinBoLLActivity {
|
|||||||
loadNonCriticalViewDelayed();
|
loadNonCriticalViewDelayed();
|
||||||
|
|
||||||
// 权限申请
|
// 权限申请
|
||||||
PermissionUtils.getInstance().checkAndRequestStoragePermission(this);
|
if (!PermissionUtils.getInstance().hasFullStoragePermission()) {
|
||||||
|
PermissionUtils.getInstance().requestFullStoragePermission(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除 onSaveInstanceState 方法
|
// 移除 onSaveInstanceState 方法
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
import cc.winboll.studio.libaes.views.AToolbar;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
import cc.winboll.studio.powerbell.App;
|
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.dialogs.YesNoAlertDialog;
|
||||||
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
import cc.winboll.studio.powerbell.model.BackgroundBean;
|
||||||
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
|
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.ImageCropUtils;
|
||||||
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||||
|
import cc.winboll.studio.powerbell.utils.UriUtil;
|
||||||
import cc.winboll.studio.powerbell.views.BackgroundView;
|
import cc.winboll.studio.powerbell.views.BackgroundView;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
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 {
|
public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
|
||||||
|
|
||||||
@@ -111,7 +109,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initToolbar() {
|
private void initToolbar() {
|
||||||
mToolbar = findViewById(R.id.toolbar);
|
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(mToolbar);
|
setSupportActionBar(mToolbar);
|
||||||
mToolbar.setSubtitle(getTag());
|
mToolbar.setSubtitle(getTag());
|
||||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||||
@@ -175,44 +173,39 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ====================== 核心修改:Java 7 兼容的相册选择点击事件 ======================
|
||||||
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
LogUtils.d(TAG, "【按钮点击】选择图片");
|
LogUtils.d(TAG, "【按钮点击】选择图片");
|
||||||
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
|
// 适配 API 30+ 权限逻辑
|
||||||
LogUtils.d(TAG, "【选图权限】已获取");
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
Intent[] intents = new Intent[3];
|
// API 30+ 优先判断全文件管理权限
|
||||||
Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
if (!Environment.isExternalStorageManager()) {
|
||||||
getContentIntent.setType("image/*");
|
mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this);
|
||||||
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
return;
|
||||||
getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
}
|
||||||
intents[0] = getContentIntent;
|
} else {
|
||||||
|
// 低版本判断传统读写权限
|
||||||
|
if (!mPermissionUtils.hasFullStoragePermission()) {
|
||||||
|
mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 30+ 推荐的标准化相册选择 Intent
|
||||||
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||||
pickIntent.setType("image/*");
|
pickIntent.setType("image/*");
|
||||||
|
// 添加权限 Flag,确保 Uri 可读取
|
||||||
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
intents[1] = pickIntent;
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
// 持久化 Uri 权限,避免后续操作失效
|
||||||
openDocIntent.setType("image/*");
|
pickIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||||
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 chooser = Intent.createChooser(pickIntent, "选择图片");
|
||||||
Intent intent = intents[i];
|
if (chooser.resolveActivity(getPackageManager()) != null) {
|
||||||
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);
|
startActivityForResult(chooser, REQUEST_SELECT_PICTURE);
|
||||||
LogUtils.d(TAG, "【选图意图】启动图片选择");
|
LogUtils.d(TAG, "【选图意图】启动图片选择");
|
||||||
} else {
|
} else {
|
||||||
@@ -241,9 +234,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "【选图权限】已申请");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -296,7 +286,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
|
if (mPermissionUtils.hasFullStoragePermission()) {
|
||||||
LogUtils.d(TAG, "【拍照权限】已获取");
|
LogUtils.d(TAG, "【拍照权限】已获取");
|
||||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||||
try {
|
try {
|
||||||
@@ -310,6 +300,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
LogUtils.e(TAG, "【拍照失败】");
|
LogUtils.e(TAG, "【拍照失败】");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
mPermissionUtils.requestFullStoragePermission(BackgroundSettingsActivity.this);
|
||||||
LogUtils.d(TAG, "【拍照权限】已申请");
|
LogUtils.d(TAG, "【拍照权限】已申请");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,7 +352,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode);
|
LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode);
|
||||||
|
|
||||||
try {
|
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();
|
handleStoragePermissionCallback();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -402,12 +393,8 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleTakePhotoResult(int resultCode, Intent data) {
|
private void handleTakePhotoResult(int resultCode, Intent data) {
|
||||||
if (resultCode != RESULT_OK || data == null) {
|
if (resultCode != RESULT_OK || !mfTakePhoto.exists() || mfTakePhoto.length() <= 0) {
|
||||||
handleOperationCancelOrFail();
|
handleOperationCancelOrFail();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) {
|
|
||||||
ToastUtils.show("拍照文件无效");
|
ToastUtils.show("拍照文件无效");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -512,9 +499,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
return FileUtils.copyFile(srcFile, dstFile);
|
return FileUtils.copyFile(srcFile, dstFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 核心修改:裁剪成功后调用双重刷新,确保最新图片加载
|
|
||||||
*/
|
|
||||||
private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
|
private void handleCropImageResult(int requestCode, int resultCode, Intent data) {
|
||||||
LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果");
|
LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果");
|
||||||
File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath());
|
File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath());
|
||||||
@@ -547,7 +531,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键修改:延迟300ms调用双重刷新,确保文件写入完成
|
// 延迟300ms调用双重刷新,确保文件写入完成
|
||||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -699,9 +683,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
return cropBitmap;
|
return cropBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 核心修改:添加缓存清理 + 控件 Bitmap 清空逻辑
|
|
||||||
*/
|
|
||||||
private void doubleRefreshPreview() {
|
private void doubleRefreshPreview() {
|
||||||
LogUtils.d(TAG, "【双重刷新】开始");
|
LogUtils.d(TAG, "【双重刷新】开始");
|
||||||
// 1. 清空缓存工具类的旧数据
|
// 1. 清空缓存工具类的旧数据
|
||||||
@@ -717,22 +698,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 清空 BackgroundView 自身的 Bitmap 引用
|
// 2. 重新加载最新数据
|
||||||
// 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. 重新加载最新数据
|
|
||||||
if (mBackgroundView != null && !isFinishing()) {
|
if (mBackgroundView != null && !isFinishing()) {
|
||||||
mBgSourceUtils.loadSettings();
|
mBgSourceUtils.loadSettings();
|
||||||
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
|
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean());
|
||||||
@@ -742,7 +708,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 延长延迟到200ms,确保文件完全加载
|
// 3. 延长延迟到200ms,确保文件完全加载
|
||||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -755,22 +721,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void handleOperationCancelOrFail() {
|
private void handleOperationCancelOrFail() {
|
||||||
mBgSourceUtils.setCurrentSourceToPreview();
|
mBgSourceUtils.setCurrentSourceToPreview();
|
||||||
LogUtils.d(TAG, "【操作回调】取消或失败");
|
LogUtils.d(TAG, "【操作回调】取消或失败");
|
||||||
ToastUtils.show("【操作回调】取消或失败");
|
ToastUtils.show("操作取消或失败");
|
||||||
doubleRefreshPreview();
|
doubleRefreshPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取FileProvider Uri(复用方法,避免重复代码)
|
|
||||||
*/
|
|
||||||
public Uri getFileProviderUri(File file) {
|
public Uri getFileProviderUri(File file) {
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
|
String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider";
|
||||||
|
|
||||||
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
|
return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file);
|
||||||
} else {
|
} else {
|
||||||
return Uri.fromFile(file);
|
return Uri.fromFile(file);
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
|
|||||||
|
|
||||||
public void onCheckPermission(View view) {
|
public void onCheckPermission(View view) {
|
||||||
//ToastUtils.show("onCheckPermission");
|
//ToastUtils.show("onCheckPermission");
|
||||||
if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
|
if (PermissionUtils.getInstance().hasFullStoragePermission()) {
|
||||||
ToastUtils.show("【权限检查】存储权限已全部获取");
|
ToastUtils.show("【权限检查】存储权限已全部获取");
|
||||||
|
} else {
|
||||||
|
PermissionUtils.getInstance().requestFullStoragePermission(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,214 +1,113 @@
|
|||||||
package cc.winboll.studio.powerbell.utils;
|
package cc.winboll.studio.powerbell.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Context;
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.widget.Toast;
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
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>
|
* 存储权限工具类(适配 API 30+)
|
||||||
* @Date 2025/12/01 16:05
|
* 核心支持:1. 传统读写权限 2. MANAGE_EXTERNAL_STORAGE 全文件权限
|
||||||
* @Describe 权限申请工具类(单例)
|
|
||||||
* 核心特性:
|
|
||||||
* 1. 适配全Android版本(6.0+ 动态权限 / 13+ 兼容)
|
|
||||||
* 2. 支持多包名场景(无硬编码包名)
|
|
||||||
* 3. 统一权限校验、申请、回调处理
|
|
||||||
* 4. 自带用户引导(拒绝权限+不再询问场景)
|
|
||||||
*/
|
*/
|
||||||
public class PermissionUtils {
|
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 static volatile PermissionUtils sInstance;
|
||||||
|
private Context mContext;
|
||||||
|
|
||||||
// 私有构造(禁止外部实例化)
|
// 权限请求码
|
||||||
private PermissionUtils() {}
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单例实例(适配多包名,无硬编码)
|
|
||||||
*/
|
|
||||||
public static PermissionUtils getInstance() {
|
public static PermissionUtils getInstance() {
|
||||||
if (sInstance == null) {
|
if (sInstance == null) {
|
||||||
synchronized (PermissionUtils.class) {
|
throw new IllegalStateException("请先调用 init(Context) 初始化");
|
||||||
if (sInstance == null) {
|
|
||||||
sInstance = new PermissionUtils();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================== 存储权限核心方法 ========================================
|
public static void init(Context context) {
|
||||||
/**
|
if (sInstance == null) {
|
||||||
* 检查并申请存储权限(统一入口,适配全Android版本)
|
synchronized (PermissionUtils.class) {
|
||||||
* @param activity 上下文(用于权限申请和弹窗)
|
if (sInstance == null) {
|
||||||
* @return true:权限已全部获取;false:需要申请权限
|
sInstance = new PermissionUtils(context);
|
||||||
*/
|
|
||||||
public boolean checkAndRequestStoragePermission(Activity activity) {
|
|
||||||
if (activity == null || activity.isFinishing()) {
|
|
||||||
LogUtils.e(TAG, "【权限检查】Activity为空或已销毁,权限检查失败");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT);
|
|
||||||
|
|
||||||
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号)
|
|
||||||
String[] requiredPermissions = {
|
|
||||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
|
||||||
android.Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
};
|
|
||||||
|
|
||||||
// 筛选未授予的权限
|
|
||||||
List<String> needPermissions = new ArrayList<>();
|
|
||||||
for (String permission : requiredPermissions) {
|
|
||||||
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
needPermissions.add(permission);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无权限需要申请:触发动态申请
|
|
||||||
if (!needPermissions.isEmpty()) {
|
|
||||||
String[] permissionsArr = needPermissions.toArray(new String[0]);
|
|
||||||
ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2);
|
|
||||||
LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有权限已授予
|
|
||||||
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理存储权限申请回调(统一逻辑,无需在Activity中重复编写)
|
* 检查是否拥有完整的文件管理权限
|
||||||
* @param activity 上下文
|
|
||||||
* @param requestCode 请求码(匹配STORAGE_PERMISSION_REQUEST)
|
|
||||||
* @param permissions 申请的权限数组
|
|
||||||
* @param grantResults 权限授予结果数组
|
|
||||||
* @return true:回调已处理;false:非当前工具类的权限回调
|
|
||||||
*/
|
*/
|
||||||
public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
|
public boolean hasFullStoragePermission() {
|
||||||
// 过滤非存储权限回调
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
if (requestCode != STORAGE_PERMISSION_REQUEST2) {
|
// API 30+ 需判断 MANAGE_EXTERNAL_STORAGE 权限
|
||||||
return false;
|
return Environment.isExternalStorageManager();
|
||||||
}
|
|
||||||
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 {
|
} else {
|
||||||
// 部分/全部拒绝:判断是否勾选“不再询问”
|
// 低版本判断传统读写权限
|
||||||
boolean shouldShowRationale = false;
|
int read = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||||
for (String permission : permissions) {
|
int write = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||||
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
|
return read == PackageManager.PERMISSION_GRANTED && write == PackageManager.PERMISSION_GRANTED;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限
|
* 申请文件管理权限(自动适配系统版本)
|
||||||
|
* @param activity 发起申请的 Activity
|
||||||
*/
|
*/
|
||||||
private void showPermissionSettingDialog(final Activity activity) {
|
public void requestFullStoragePermission(android.app.Activity activity) {
|
||||||
new AlertDialog.Builder(activity)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
.setTitle(activity.getString(R.string.permission_denied_title))
|
// API 30+ 跳转系统设置页开启权限
|
||||||
.setMessage(activity.getString(R.string.permission_storage_setting_guide))
|
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||||
.setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() {
|
Uri uri = Uri.fromParts("package", mContext.getPackageName(), null);
|
||||||
@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);
|
intent.setData(uri);
|
||||||
activity.startActivity(intent);
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.setNegativeButton(activity.getString(R.string.cancel), null)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================== 扩展:其他权限方法(可选) ========================================
|
|
||||||
/**
|
/**
|
||||||
* 检查单个权限是否已授予(通用方法,可复用)
|
* 处理低版本权限申请结果(在 Activity 的 onRequestPermissionsResult 中调用)
|
||||||
|
* @param activity 上下文
|
||||||
|
* @param requestCode 请求码
|
||||||
|
* @param permissions 权限数组
|
||||||
|
* @param grantResults 授权结果
|
||||||
|
* @return 权限是否申请成功
|
||||||
*/
|
*/
|
||||||
public boolean isPermissionGranted(Activity activity, String permission) {
|
public boolean handleStoragePermissionResult(android.app.Activity activity, int requestCode, String[] permissions, int[] grantResults) {
|
||||||
if (activity == null || TextUtils.isEmpty(permission)) {
|
// 仅处理传统存储权限的请求结果
|
||||||
|
if (requestCode != REQUEST_CODE_STORAGE) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
|
// 校验授权结果:读写权限均需授予
|
||||||
}
|
boolean isGranted = grantResults.length > 0
|
||||||
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||||
|
&& grantResults[1] == PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
/**
|
if (isGranted) {
|
||||||
* 申请单个权限(通用方法,可复用)
|
// 权限授予成功,提示用户
|
||||||
*/
|
Toast.makeText(activity, "存储权限申请成功", Toast.LENGTH_SHORT).show();
|
||||||
public void requestSinglePermission(Activity activity, String permission, int requestCode) {
|
} else {
|
||||||
if (activity == null || TextUtils.isEmpty(permission)) {
|
// 权限被拒绝,引导用户手动开启
|
||||||
return;
|
Toast.makeText(activity, "存储权限被拒绝,部分功能无法使用", Toast.LENGTH_SHORT).show();
|
||||||
}
|
|
||||||
if (!isPermissionGranted(activity, permission)) {
|
|
||||||
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
|
|
||||||
}
|
}
|
||||||
|
return isGranted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -105,16 +107,81 @@ public class UriUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getUriForFile(Context context, String filePath) {
|
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);
|
File file = new File(filePath);
|
||||||
|
// 2. 打印File对象的绝对路径和存在性
|
||||||
|
LogUtils.d(TAG, "getUriForFile -> 文件绝对路径:" + file.getAbsolutePath());
|
||||||
|
LogUtils.d(TAG, "getUriForFile -> 文件是否存在:" + file.exists());
|
||||||
|
LogUtils.d(TAG, "getUriForFile -> 是否为目录:" + file.isDirectory());
|
||||||
|
|
||||||
|
// 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);
|
return getUriForFile(context, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getUriForFile(Context context, File file) {
|
public static Uri getUriForFile(Context context, File file) {
|
||||||
//Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
if (context == null) {
|
||||||
if (Build.VERSION.SDK_INT >= 24) {//android 7.0以上
|
LogUtils.e(TAG, "getUriForFile -> Context为空");
|
||||||
return FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
|
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;
|
||||||
}
|
}
|
||||||
return Uri.fromFile(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
|
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName)
|
||||||
|
|||||||
Reference in New Issue
Block a user