Compare commits
12 Commits
powerbell-
...
powerbell-
| Author | SHA1 | Date | |
|---|---|---|---|
| a44f7fe6d4 | |||
| 35a34b5b53 | |||
| d35d0d0291 | |||
| 03212c0554 | |||
| 0c963213df | |||
| 10ddca4f73 | |||
| f240d9c057 | |||
| 2c77bf775b | |||
| 1db7c9bf80 | |||
| fd556fd06f | |||
| 220aa9dbfb | |||
| ecafd2026f |
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Thu Dec 11 03:22:10 HKT 2025
|
||||
stageCount=15
|
||||
#Thu Dec 11 20:54:28 HKT 2025
|
||||
stageCount=16
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.14
|
||||
publishVersion=15.12.15
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.15
|
||||
baseBetaVersion=15.12.16
|
||||
|
||||
BIN
powerbell/src/main/assets/images/blank10x10.png
Normal file
BIN
powerbell/src/main/assets/images/blank10x10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 B |
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -60,7 +58,7 @@ public class MainActivity extends WinBoLLActivity {
|
||||
|
||||
// ======================== 静态常量(移除缓存相关键)========================
|
||||
public static final String TAG = "MainActivity";
|
||||
private static final int REQUEST_WRITE_STORAGE_PERMISSION = 1001;
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
public static final int MSG_RELOAD_APPCONFIG = 0;
|
||||
public static final int MSG_CURRENTVALUEBATTERY = 1;
|
||||
public static final int MSG_LOAD_BACKGROUND = 2;
|
||||
@@ -125,8 +123,8 @@ public class MainActivity extends WinBoLLActivity {
|
||||
loadNonCriticalViewDelayed();
|
||||
|
||||
// 权限申请
|
||||
PermissionUtils.getInstance().checkAndRequestStoragePermission(this);
|
||||
}
|
||||
PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES);
|
||||
}
|
||||
|
||||
// 移除 onSaveInstanceState 方法
|
||||
// 移除 onRestoreInstanceState 方法
|
||||
@@ -163,6 +161,7 @@ public class MainActivity extends WinBoLLActivity {
|
||||
AESThemeUtil.inflateMenu(this, menu);
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu);
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, mMenu);
|
||||
return true;
|
||||
@@ -217,6 +216,14 @@ public class MainActivity extends WinBoLLActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
|
||||
PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
moveTaskToBack(true);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import cc.winboll.studio.powerbell.utils.PermissionUtils;
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "SettingsActivity";
|
||||
private static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
@@ -52,8 +53,15 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
|
||||
public void onCheckPermission(View view) {
|
||||
//ToastUtils.show("onCheckPermission");
|
||||
if (PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) {
|
||||
ToastUtils.show("【权限检查】存储权限已全部获取");
|
||||
PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
|
||||
PermissionUtils.getInstance().handleStoragePermissionResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
BackgroundView mBackgroundView;
|
||||
// 测试图片路径(用Environment获取,适配低版本,避免硬编码)
|
||||
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
|
||||
+ "/PowerBell/unittest/1764946782079.jpeg";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -90,7 +90,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
|
||||
}
|
||||
String dstOutputPath = outputDir.getAbsolutePath()
|
||||
+ "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
|
||||
+ "/1764946782079-crop.jpeg";
|
||||
|
||||
// 修复3:自由裁剪时比例传0(避免100:100过大导致机型崩溃)
|
||||
ImageCropUtils.startImageCrop(
|
||||
@@ -161,7 +161,7 @@ public class MainUnitTestActivity extends AppCompatActivity {
|
||||
// 裁剪完成后回收权限
|
||||
if (requestCode == REQUEST_CROP_IMAGE) {
|
||||
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
||||
+ "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg";
|
||||
+ "/PowerBell/unittest/1764946782079-crop.jpeg";
|
||||
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
|
||||
//ImageCropUtils.releaseCropPermission(this, outputUri);
|
||||
mBackgroundView.loadImage(dstOutputPath);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/11 09:14
|
||||
* @Describe Assets 目录拷贝工具类
|
||||
* 支持将 assets/images/ 下所有文件、子目录拷贝到指定路径
|
||||
*/
|
||||
public class AssetsCopyUtils {
|
||||
public static final String TAG = "AssetsCopyUtils";
|
||||
private static final int BUFFER_SIZE = 1024 * 8;
|
||||
|
||||
/**
|
||||
* 拷贝 assets/images/ 目录到指定目标目录
|
||||
* @param context 上下文
|
||||
* @param targetDirPath 目标目录完整路径(如 /sdcard/PowerBell/assets_images)
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsImagesToDir(Context context, String targetDirPath) {
|
||||
// 拷贝 assets/images 根目录
|
||||
return copyAssetsDirToDir(context, "images", targetDirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归拷贝 assets 下指定目录到目标目录
|
||||
* @param context 上下文
|
||||
* @param assetsDir assets 下的源目录(如 "images"、"images/subdir")
|
||||
* @param targetDirPath 目标目录完整路径
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsDirToDir(Context context, String assetsDir, String targetDirPath) {
|
||||
File targetDir = new File(targetDirPath);
|
||||
// 创建目标目录(含多级父目录)
|
||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||
Log.e(TAG, "创建目标目录失败:" + targetDirPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取 assets 目录下的文件/子目录列表
|
||||
String[] fileList = context.getAssets().list(assetsDir);
|
||||
if (fileList == null || fileList.length == 0) {
|
||||
Log.d(TAG, "assets 目录为空:" + assetsDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String fileName : fileList) {
|
||||
String assetsFilePath = assetsDir + File.separator + fileName;
|
||||
String targetFilePath = targetDirPath + File.separator + fileName;
|
||||
|
||||
// 判断当前项是文件还是子目录
|
||||
String[] subFileList = context.getAssets().list(assetsFilePath);
|
||||
if (subFileList != null && subFileList.length > 0) {
|
||||
// 是子目录,递归拷贝
|
||||
if (!copyAssetsDirToDir(context, assetsFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 是文件,直接拷贝
|
||||
if (!copyAssetsFileToDir(context, assetsFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "assets 目录拷贝完成:" + assetsDir + " -> " + targetDirPath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝 assets 目录异常:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝 assets 下单个文件到指定路径
|
||||
* @param context 上下文
|
||||
* @param assetsFilePath assets 下的文件路径(如 "images/cloud.png")
|
||||
* @param targetFilePath 目标文件完整路径
|
||||
* @return 拷贝是否成功
|
||||
*/
|
||||
public static boolean copyAssetsFileToDir(Context context, String assetsFilePath, String targetFilePath) {
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetsFilePath);
|
||||
File targetFile = new File(targetFilePath);
|
||||
// 覆盖已存在的文件
|
||||
if (targetFile.exists() && !targetFile.delete()) {
|
||||
Log.w(TAG, "覆盖目标文件失败,跳过:" + targetFilePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
outputStream = new FileOutputStream(targetFile);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
Log.d(TAG, "文件拷贝成功:" + assetsFilePath + " -> " + targetFilePath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "拷贝文件失败:" + assetsFilePath + ",异常:" + e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
// 关闭流
|
||||
try {
|
||||
if (inputStream != null) inputStream.close();
|
||||
if (outputStream != null) outputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "关闭流异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
* 特点:1. 单例模式 2. 压缩加载避免OOM 3. 路径- Bitmap 映射 4. 线程安全
|
||||
*/
|
||||
public class BitmapCacheUtils {
|
||||
private static final String TAG = "BitmapCacheUtils";
|
||||
public static final String TAG = "BitmapCacheUtils";
|
||||
// 最大图片尺寸(适配1080P屏幕,可根据需求调整)
|
||||
private static final int MAX_WIDTH = 1080;
|
||||
private static final int MAX_HEIGHT = 1920;
|
||||
@@ -50,7 +50,7 @@ public class BitmapCacheUtils {
|
||||
/**
|
||||
* 核心接口:根据图片路径缓存 Bitmap 到内存
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存成功的 Bitmap / null(路径无效/文件不存在)
|
||||
* @return 缓存成功的 Bitmap / null(路径无效/文件不存在/解码失败)
|
||||
*/
|
||||
public Bitmap cacheBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
@@ -59,15 +59,23 @@ public class BitmapCacheUtils {
|
||||
}
|
||||
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile()) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片文件不存在 - " + imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "cacheBitmap: 图片文件无效(不存在/非文件/空文件) - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 已缓存则直接返回,避免重复加载
|
||||
if (mBitmapCacheMap.containsKey(imagePath)) {
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath);
|
||||
return mBitmapCacheMap.get(imagePath);
|
||||
Bitmap cachedBitmap = mBitmapCacheMap.get(imagePath);
|
||||
// 额外校验缓存的Bitmap是否有效
|
||||
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
|
||||
LogUtils.d(TAG, "cacheBitmap: 图片已缓存,直接返回 - " + imagePath);
|
||||
return cachedBitmap;
|
||||
} else {
|
||||
// 缓存的Bitmap已失效,移除后重新加载
|
||||
mBitmapCacheMap.remove(imagePath);
|
||||
LogUtils.w(TAG, "cacheBitmap: 缓存Bitmap已失效,移除后重新加载 - " + imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 压缩加载 Bitmap(避免OOM)
|
||||
@@ -85,25 +93,33 @@ public class BitmapCacheUtils {
|
||||
/**
|
||||
* 核心接口:根据路径获取缓存的 Bitmap
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 缓存的 Bitmap / null
|
||||
* @return 缓存的有效 Bitmap / null(未缓存/已回收)
|
||||
*/
|
||||
public Bitmap getCachedBitmap(String imagePath) {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return null;
|
||||
}
|
||||
return mBitmapCacheMap.get(imagePath);
|
||||
Bitmap bitmap = mBitmapCacheMap.get(imagePath);
|
||||
// 校验Bitmap是否有效
|
||||
if (bitmap != null && bitmap.isRecycled()) {
|
||||
mBitmapCacheMap.remove(imagePath);
|
||||
return null;
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有 Bitmap 缓存(释放内存)
|
||||
*/
|
||||
public void clearAllCache() {
|
||||
for (Bitmap bitmap : mBitmapCacheMap.values()) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle(); // 主动回收 Bitmap
|
||||
synchronized (mBitmapCacheMap) {
|
||||
for (Bitmap bitmap : mBitmapCacheMap.values()) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle(); // 主动回收 Bitmap
|
||||
}
|
||||
}
|
||||
mBitmapCacheMap.clear();
|
||||
}
|
||||
mBitmapCacheMap.clear();
|
||||
LogUtils.d(TAG, "clearAllCache: 所有 Bitmap 缓存已清空");
|
||||
}
|
||||
|
||||
@@ -115,22 +131,39 @@ public class BitmapCacheUtils {
|
||||
if (TextUtils.isEmpty(imagePath)) {
|
||||
return;
|
||||
}
|
||||
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
|
||||
synchronized (mBitmapCacheMap) {
|
||||
Bitmap bitmap = mBitmapCacheMap.remove(imagePath);
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
LogUtils.d(TAG, "removeCachedBitmap: 移除并回收缓存 - " + imagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩解码 Bitmap(按最大尺寸缩放,避免OOM)
|
||||
* @param imagePath 图片绝对路径
|
||||
* @return 解码后的 Bitmap / null(文件无效/解码失败)
|
||||
*/
|
||||
private Bitmap decodeCompressedBitmap(String imagePath) {
|
||||
// 前置校验:确保文件有效
|
||||
File imageFile = new File(imagePath);
|
||||
if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 文件无效,跳过解码 - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
// 第一步:只获取图片尺寸,不加载像素
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(imagePath, options);
|
||||
|
||||
// 校验尺寸是否有效
|
||||
if (options.outWidth <= 0 || options.outHeight <= 0) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 图片尺寸无效 - " + imagePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算缩放比例
|
||||
int sampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT);
|
||||
|
||||
@@ -141,7 +174,15 @@ public class BitmapCacheUtils {
|
||||
options.inPurgeable = true;
|
||||
options.inInputShareable = true;
|
||||
|
||||
return BitmapFactory.decodeFile(imagePath, options);
|
||||
try {
|
||||
return BitmapFactory.decodeFile(imagePath, options);
|
||||
} catch (OutOfMemoryError e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: OOM异常 - " + imagePath);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "decodeCompressedBitmap: 解码异常 - " + imagePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,214 +1,134 @@
|
||||
package cc.winboll.studio.powerbell.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
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. 自带用户引导(拒绝权限+不再询问场景)
|
||||
* 权限申请工具类
|
||||
* 适配 Android 13+ 媒体权限 & 低版本存储权限
|
||||
* 兼容 SDK 版本低于 33 的编译环境
|
||||
*/
|
||||
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 final String TAG = "PermissionUtils";
|
||||
// 存储权限请求码
|
||||
public static final int REQUEST_STORAGE = 1000;
|
||||
// 媒体图片权限请求码(Android 13+)
|
||||
public static final int REQUEST_READ_MEDIA_IMAGES = 1001;
|
||||
|
||||
// 单例实例(双重校验锁,线程安全)
|
||||
private static volatile PermissionUtils sInstance;
|
||||
// 手动定义 Android 13+ 媒体图片权限常量(解决 SDK 低于 33 无法识别问题)
|
||||
private static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES";
|
||||
// Android 13 对应的 SDK 版本号(替代 Build.VERSION_CODES.TIRAMISU)
|
||||
private static final int SDK_VERSION_TIRAMISU = 33;
|
||||
|
||||
// 私有构造(禁止外部实例化)
|
||||
private PermissionUtils() {}
|
||||
// 单例模式
|
||||
private static volatile PermissionUtils sInstance;
|
||||
|
||||
/**
|
||||
* 获取单例实例(适配多包名,无硬编码)
|
||||
*/
|
||||
public static PermissionUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (PermissionUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PermissionUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
private PermissionUtils() {}
|
||||
|
||||
// ======================================== 存储权限核心方法 ========================================
|
||||
/**
|
||||
* 检查并申请存储权限(统一入口,适配全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 PermissionUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (PermissionUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new PermissionUtils();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号)
|
||||
String[] requiredPermissions = {
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
};
|
||||
/**
|
||||
* 检查并请求 存储权限(Android 12及以下)
|
||||
*/
|
||||
public boolean checkAndRequestStoragePermission(Activity activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11+ 无需申请 READ_EXTERNAL_STORAGE,直接返回true
|
||||
return true;
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
String[] permissions = {
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
};
|
||||
if (ContextCompat.checkSelfPermission(activity, permissions[0]) != PackageManager.PERMISSION_GRANTED
|
||||
|| ContextCompat.checkSelfPermission(activity, permissions[1]) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity, permissions, REQUEST_STORAGE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 筛选未授予的权限
|
||||
List<String> needPermissions = new ArrayList<>();
|
||||
for (String permission : requiredPermissions) {
|
||||
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
needPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 检查并请求 媒体图片权限(Android 13+)
|
||||
* 兼容 SDK 编译版本低于 33 的情况
|
||||
*/
|
||||
public boolean checkAndRequestMediaImagesPermission(Activity activity, int requestCode) {
|
||||
// 用数值 33 替代 Build.VERSION_CODES.TIRAMISU
|
||||
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
|
||||
// 用手动定义的权限常量替代 android.Manifest.permission.READ_MEDIA_IMAGES
|
||||
if (ContextCompat.checkSelfPermission(activity, READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(activity, new String[]{READ_MEDIA_IMAGES}, requestCode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 低版本已通过存储权限覆盖,直接返回true
|
||||
return true;
|
||||
}
|
||||
|
||||
// 无权限需要申请:触发动态申请
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 权限请求结果处理
|
||||
*/
|
||||
public void handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case REQUEST_STORAGE:
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "存储权限申请成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "存储权限申请失败");
|
||||
}
|
||||
break;
|
||||
case REQUEST_READ_MEDIA_IMAGES:
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.d(TAG, "媒体图片权限申请成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "媒体图片权限申请失败");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LogUtils.d(TAG, "未知权限请求码:" + requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
// 所有权限已授予
|
||||
LogUtils.d(TAG, "【权限检查】存储权限已全部获取");
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 检查是否有管理所有文件权限(Android 11+)
|
||||
*/
|
||||
public boolean checkManageExternalStoragePermission(Activity activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return android.os.Environment.isExternalStorageManager();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 请求管理所有文件权限(Android 11+)
|
||||
*/
|
||||
public void requestManageExternalStoragePermission(Activity activity, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
android.content.Intent intent = new android.content.Intent(
|
||||
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
||||
);
|
||||
intent.setData(android.net.Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "请求管理文件权限异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -104,6 +104,10 @@ public class BackgroundView extends RelativeLayout {
|
||||
}
|
||||
|
||||
public void loadBackgroundBean(BackgroundBean bean) {
|
||||
loadBackgroundBean(bean, false);
|
||||
}
|
||||
|
||||
public void loadBackgroundBean(BackgroundBean bean, boolean isRefresh) {
|
||||
if (!bean.isUseBackgroundFile()) {
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
@@ -111,13 +115,23 @@ public class BackgroundView extends RelativeLayout {
|
||||
String targetPath = bean.isUseBackgroundScaledCompressFile()
|
||||
? bean.getBackgroundScaledCompressFilePath()
|
||||
: bean.getBackgroundFilePath();
|
||||
// 调用带路径判断的loadImage方法
|
||||
loadImage(targetPath);
|
||||
|
||||
if (!(new File(targetPath).exists())) {
|
||||
LogUtils.d(TAG, String.format("视图控件图片不存在:%s", targetPath));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用带路径判断的loadImage方法
|
||||
if (isRefresh) {
|
||||
App._mBitmapCacheUtils.removeCachedBitmap(targetPath);
|
||||
App._mBitmapCacheUtils.cacheBitmap(targetPath);
|
||||
}
|
||||
loadImage(targetPath);
|
||||
}
|
||||
|
||||
// ====================================== 对外方法 ======================================
|
||||
/**
|
||||
* 改造后:添加路径判断,路径更新时同步更新缓存
|
||||
* 改造后:添加路径判断,路径更新时同步更新缓存;缓存Bitmap为null时提示并加载透明背景
|
||||
* @param imagePath 图片绝对路径
|
||||
*/
|
||||
public void loadImage(String imagePath) {
|
||||
@@ -139,8 +153,8 @@ public class BackgroundView extends RelativeLayout {
|
||||
// ======================== 新增:路径判断逻辑 ========================
|
||||
// 1. 路径未变化:直接使用缓存
|
||||
if (imagePath.equals(mCurrentCachedPath)) {
|
||||
//ToastUtils.show("isReload == false");
|
||||
Bitmap cachedBitmap = App._mBitmapCacheUtils.getCachedBitmap(imagePath);
|
||||
// 核心修改:判断缓存Bitmap是否为null
|
||||
if (cachedBitmap != null && !cachedBitmap.isRecycled()) {
|
||||
LogUtils.d(TAG, "loadImage: 路径未变,使用缓存 Bitmap");
|
||||
mImageAspectRatio = (float) cachedBitmap.getWidth() / cachedBitmap.getHeight();
|
||||
@@ -148,7 +162,13 @@ public class BackgroundView extends RelativeLayout {
|
||||
adjustImageViewSize();
|
||||
mIvBackground.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 缓存Bitmap为空或已回收,提示并加载透明背景
|
||||
LogUtils.e(TAG, "loadImage: 全局位图缓存为空或已回收 - " + imagePath);
|
||||
ToastUtils.show("全局位图缓存为空,无法加载图片");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 路径已更新:移除旧缓存,加载新图片并更新缓存
|
||||
@@ -166,6 +186,8 @@ public class BackgroundView extends RelativeLayout {
|
||||
|
||||
Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920);
|
||||
if (bitmap == null) {
|
||||
LogUtils.e(TAG, "loadImage: 图片解码失败");
|
||||
ToastUtils.show("图片解码失败,无法加载");
|
||||
setDefaultTransparentBackground();
|
||||
return;
|
||||
}
|
||||
@@ -258,6 +280,7 @@ public class BackgroundView extends RelativeLayout {
|
||||
mImageAspectRatio = 1.0f;
|
||||
// 清空缓存路径记录
|
||||
mCurrentCachedPath = "";
|
||||
mIvBackground.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// ====================================== 重写方法 ======================================
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_log"
|
||||
android:title="@string/item_logview"/>
|
||||
<item
|
||||
android:id="@+id/action_unittestactivity"
|
||||
android:title="@string/item_mainunittestactivity"/>
|
||||
|
||||
Reference in New Issue
Block a user