Compare commits

..

11 Commits

12 changed files with 295 additions and 242 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Dec 13 13:05:44 GMT 2025
stageCount=0
#Sun Dec 14 04:55:45 HKT 2025
stageCount=3
libraryProject=
baseVersion=15.14
publishVersion=15.14.0
buildCount=1
baseBetaVersion=15.14.1
publishVersion=15.14.2
buildCount=0
baseBetaVersion=15.14.3

View File

@@ -4,24 +4,12 @@
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 开机启动 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<!-- 显示通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
@@ -31,9 +19,6 @@
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<!-- 计算应用存储空间 -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
@@ -233,4 +218,4 @@
</application>
</manifest>
</manifest>

View File

@@ -14,7 +14,6 @@ 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 {

View File

@@ -25,12 +25,14 @@ 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.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libaes.models.APPInfo;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libaes.views.ADsBannerView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
@@ -68,6 +70,7 @@ public class MainActivity extends WinBoLLActivity {
public static MainActivity _mMainActivity;
static MainViewFragment _mMainViewFragment;
static Handler _mHandler;
PermissionUtils permissionUtils = PermissionUtils.getInstance();
private App mApplication;
private AppConfigUtils mAppConfigUtils;
@@ -121,13 +124,26 @@ public class MainActivity extends WinBoLLActivity {
initViewHolder();
initCriticalView();
loadNonCriticalViewDelayed();
// 权限申请
PermissionUtils.getInstance().checkAndRequestMediaImagesPermission(this, REQUEST_READ_MEDIA_IMAGES);
}
// 移除 onSaveInstanceState 方法
// 移除 onRestoreInstanceState 方法
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// 电池优化权限(通用所有机型)
if (!permissionUtils.checkIgnoreBatteryOptimizationPermission(this)) {
YesNoAlertDialog.show(this, getString(R.string.app_name) + "权限申请提示:", "本应用要正常使用,需要申请电池优化与自启动权限。是否进入权限设置步骤?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onNo() {
ToastUtils.show(getString(R.string.app_name) + "应用可能无法正常使用。");
}
@Override
public void onYes() {
permissionUtils.requestIgnoreBatteryOptimizationPermission(MainActivity.this);
}
});
}
}
@Override
protected void onDestroy() {
@@ -189,7 +205,7 @@ public class MainActivity extends WinBoLLActivity {
startActivity(new Intent(this, ClearRecordActivity.class));
break;
case R.id.action_changepicture:
startActivity(new Intent(this, BackgroundSettingsActivity.class));
startActivityForResult(new Intent(this, BackgroundSettingsActivity.class), REQUEST_READ_MEDIA_IMAGES);
break;
case R.id.action_unittestactivity:
startActivity(new Intent(this, MainUnitTestActivity.class));
@@ -209,20 +225,20 @@ public class MainActivity extends WinBoLLActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PermissionUtils.REQUEST_IGNORE_BATTERY_OPTIMIZATION) {
// 自启动权限(小米专属)
if (permissionUtils.checkAutoStartPermission(this)) {
// 小米机型,发起自启动权限申请
permissionUtils.requestAutoStartPermission(this);
}
} else if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
if (_mHandler != null) {
_mHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
}
}
}
@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() {

View File

@@ -3,6 +3,7 @@ package cc.winboll.studio.powerbell.activities;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
@@ -15,6 +16,7 @@ import android.os.Looper;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.FileProvider;
@@ -29,7 +31,6 @@ import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.BitmapCacheUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.BufferedOutputStream;
@@ -47,11 +48,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
public static final int REQUEST_SELECT_PICTURE = 0;
public static final int REQUEST_TAKE_PHOTO = 1;
public static final int REQUEST_CROP_IMAGE = 2;
private static final int REQUEST_READ_MEDIA = 1001;
private static final int REQUEST_PIXELPICKER = 1001;
// ====================== 成员变量 ======================
private BackgroundSourceUtils mBgSourceUtils;
private PermissionUtils mPermissionUtils;
private BitmapCacheUtils mBitmapCache;
private Toolbar mToolbar;
@@ -81,7 +81,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
mBackgroundView = findViewById(R.id.background_view);
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
mPermissionUtils = PermissionUtils.getInstance();
mBitmapCache = BitmapCacheUtils.getInstance();
// 初始化临时文件与目录
@@ -91,11 +90,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
}
mfTakePhoto = new File(tempDir, "TakePhoto.jpg");
File selectTempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp");
if (!selectTempDir.exists()) {
selectTempDir.mkdirs();
LogUtils.d(TAG, "【目录初始化】选图临时目录创建完成:" + selectTempDir.getAbsolutePath());
}
// File selectTempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp");
// if (!selectTempDir.exists()) {
// selectTempDir.mkdirs();
// LogUtils.d(TAG, "【目录初始化】选图临时目录创建完成:" + selectTempDir.getAbsolutePath());
// }
// 初始化界面与事件
initToolbar();
@@ -126,11 +125,6 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
LogUtils.d(TAG, "【回调触发】requestCode" + requestCode + "resultCode" + resultCode);
try {
if (requestCode == PermissionUtils.REQUEST_READ_MEDIA_IMAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
handleStoragePermissionCallback();
return;
}
if (resultCode != RESULT_OK) {
handleOperationCancelOrFail();
return;
@@ -145,6 +139,9 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
break;
case REQUEST_CROP_IMAGE:
handleCropImageResult(requestCode, resultCode, data);
break;
case REQUEST_PIXELPICKER:
handlePixelPickerResult(requestCode, resultCode, data);
break;
default:
LogUtils.d(TAG, "【回调忽略】未知requestCode");
@@ -156,39 +153,29 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
LogUtils.d(TAG, "【权限回调】转发处理 requestCode" + requestCode);
mPermissionUtils.handleStoragePermissionResult(this, requestCode, permissions, grantResults);
}
@Override
public void finish() {
LogUtils.d(TAG, "【生命周期】finish 触发isCommitSettings" + isCommitSettings + "isPreviewBackgroundChanged" + isPreviewBackgroundChanged);
if (isCommitSettings) {
setResult(RESULT_OK);
super.finish();
} else {
if (isPreviewBackgroundChanged) {
YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener() {
@Override
public void onYes() {
//ToastUtils.show("onYes");
mBgSourceUtils.commitPreviewSourceToCurrent();
isCommitSettings = true;
setResult(RESULT_OK);
finish();
}
@Override
public void onNo() {
isCommitSettings = true;
setResult(RESULT_CANCELED);
finish();
}
});
} else {
setResult(RESULT_OK);
isCommitSettings = true;
finish();
}
@@ -238,15 +225,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】选择图片");
if (Build.VERSION.SDK_INT >= SDK_VERSION_TIRAMISU) {
if (mPermissionUtils.checkAndRequestMediaImagesPermission(BackgroundSettingsActivity.this, REQUEST_READ_MEDIA)) {
launchImageSelector();
}
} else {
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
launchImageSelector();
}
}
launchImageSelector();
}
};
@@ -296,22 +275,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
return;
}
if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) {
LogUtils.d(TAG, "【拍照权限】已获取");
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
try {
Uri photoUri = getFileProviderUri(mfTakePhoto);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
LogUtils.d(TAG, "拍照启动】Uri" + photoUri.toString());
} catch (Exception e) {
String errMsg = "拍照启动异常:" + e.getMessage();
ToastUtils.show(errMsg.substring(0, 20));
LogUtils.e(TAG, "【拍照失败】" + e.getMessage());
}
} else {
LogUtils.d(TAG, "【拍照权限】已申请");
}
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
try {
Uri photoUri = getFileProviderUri(mfTakePhoto);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
LogUtils.d(TAG, "【拍照启动】Uri" + photoUri.toString());
} catch (Exception e) {
String errMsg = "拍照启动异常" + e.getMessage();
ToastUtils.show(errMsg.substring(0, 20));
LogUtils.e(TAG, "拍照失败】" + e.getMessage());
}
}
};
@@ -324,10 +298,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
};
private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】像素拾取");
String targetImagePath = mBgSourceUtils.getCurrentBackgroundBean().getBackgroundFilePath();
String targetImagePath = mBgSourceUtils.getPreviewBackgroundBean().getBackgroundFilePath();
File targetFile = new File(targetImagePath);
if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) {
ToastUtils.show("无有效图片可拾取像素");
@@ -336,7 +311,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
}
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", targetImagePath);
startActivity(intent);
startActivityForResult(intent, REQUEST_PIXELPICKER);
LogUtils.d(TAG, "【像素拾取启动】路径:" + targetImagePath);
}
};
@@ -345,11 +320,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
BackgroundBean bean = mBgSourceUtils.getPreviewBackgroundBean();
int oldColor = bean.getPixelColor();
bean.setPixelColor(0);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, "【像素清空】旧颜色:" + oldColor);
}
@@ -395,6 +371,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
try {
mBgSourceUtils.loadSettings();
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
mBackgroundView.setBackgroundColor(mBgSourceUtils.getPreviewBackgroundBean().getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
@@ -409,6 +386,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
try {
mBgSourceUtils.loadSettings();
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
mBackgroundView.setBackgroundColor(mBgSourceUtils.getPreviewBackgroundBean().getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
@@ -881,5 +859,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
handleOperationCancelOrFail();
}
}
private void handlePixelPickerResult(int requestCode, int resultCode, Intent data) {
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
}

View File

@@ -194,7 +194,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
dialog.dismiss();
// 可以在这里添加确定后的回调逻辑
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getCurrentBackgroundBean();
BackgroundBean bean = utils.getPreviewBackgroundBean();
bean.setPixelColor(pixelColor);
utils.saveSettings();
Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show();
@@ -218,7 +218,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
void setBackgroundColor() {
BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this);
BackgroundBean bean = utils.getCurrentBackgroundBean();
BackgroundBean bean = utils.getPreviewBackgroundBean();
int nPixelColor = bean.getPixelColor();
RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1);
mainLayout.setBackgroundColor(nPixelColor);
@@ -247,9 +247,11 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi
@Override
public void onBackPressed() {
super.onBackPressed();
Intent intent = new Intent();
intent.setClass(this, BackgroundSettingsActivity.class);
startActivity(intent);
setResult(RESULT_OK);
finish();
// Intent intent = new Intent();
// intent.setClass(this, BackgroundSettingsActivity.class);
// startActivity(intent);
//GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class);
}
}

View File

@@ -6,9 +6,7 @@ import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.PermissionUtils;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
@@ -50,18 +48,4 @@ public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivit
}
});
}
public void onCheckPermission(View view) {
//ToastUtils.show("onCheckPermission");
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);
}
}
}

View File

@@ -262,7 +262,7 @@ public class ControlCenterService extends Service {
}
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
notificationManagerUtils.showTempAlertNotify(intent, getString(R.string.app_name), msg);
notificationManagerUtils.showTempAlertNotify(getString(R.string.app_name), msg);

View File

@@ -185,7 +185,7 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "【checkEmptyBackgroundAndCreateBlankBackgroundBean调用】开始检查背景Bean");
File fCheckBackgroundFile = new File(checkBackgroundBean.getBackgroundFilePath());
if (!fCheckBackgroundFile.exists()) {
String newCropFileName = "blank10x10";
String newCropFileName = genNewCropFileName();
String fileSuffix = "png";
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
@@ -211,6 +211,10 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "背景Bean文件存在无需创建空白背景");
return false;
}
String genNewCropFileName() {
return UUID.randomUUID().toString() + System.currentTimeMillis();
}
/**
* 创建并更新预览剪裁环境
@@ -228,7 +232,7 @@ public class BackgroundSourceUtils {
Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
String fileSuffix = FileUtils.getFileSuffix(mContext, uri);
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
String newCropFileName = genNewCropFileName();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
@@ -415,6 +419,7 @@ public class BackgroundSourceUtils {
*/
public void commitPreviewSourceToCurrent() {
LogUtils.d(TAG, "【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean");
//ToastUtils.show("【commitPreviewSourceToCurrent调用】开始深拷贝预览Bean到正式Bean");
currentBackgroundBean = new BackgroundBean();
currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName());
currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath());

View File

@@ -22,7 +22,7 @@ import cc.winboll.studio.powerbell.services.ControlCenterService;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/13 20:44
* @Describe 全局通知管理工具类整合所有通知能力适配API29-30兼容Java7
* @Describe 全局通知管理工具类整合所有通知能力适配API29-30兼容Java7所有通知统一跳转MainActivity
*/
public class NotificationManagerUtils {
// ====================== 常量定义(统一管理,避免冲突,首屏可见)======================
@@ -50,7 +50,7 @@ public class NotificationManagerUtils {
? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
: PendingIntent.FLAG_UPDATE_CURRENT; // API30安全标志
private static final long[] VIBRATE_PATTERN = new long[]{100, 200, 300, 400}; // 标准震动节奏
private static final String DEFAULT_JUMP_PACKAGE = "cc.winboll.studio.powerbell"; // 默认跳转包名API29+必填)
//private static final String DEFAULT_JUMP_PACKAGE = "cc.winboll.studio.powerbell"; // 默认跳转包名API29+必填)
// ====================== 成员变量(按场景分组,私有封装,避免外部篡改)======================
private final Context mContext;
@@ -117,7 +117,7 @@ public class NotificationManagerUtils {
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_BATTERY_REMIND);
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null); // 闹钟铃声
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(true); // 突破免打扰
@@ -139,7 +139,8 @@ public class NotificationManagerUtils {
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_TEMP_ALERT);
channel.setSound(null, null); // 仅震动,不发声
//channel.setSound(null, null); // 仅震动,不发声
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(false); // 不突破免打扰
@@ -151,21 +152,16 @@ public class NotificationManagerUtils {
}
/**
* 构建通用跳转PendingIntent适配API29-30安全规范,支持自定义跳转目标
* @param targetIntent 自定义跳转意图传null则默认跳MainActivity
* @return 安全的PendingIntent避免跳转失败/泄露
* 构建固定跳转PendingIntent所有通知统一跳转MainActivity适配API29-30安全规范
* @return 安全的PendingIntent确保跳转稳定不泄露
*/
private PendingIntent buildPendingIntent(Intent targetIntent) {
Intent intent = targetIntent;
// 传null则默认跳MainActivity兼容旧逻辑
if (intent == null) {
intent = new Intent(mContext, MainActivity.class);
LogUtils.d(TAG, "【Intent构建】未传自定义Intent默认跳转MainActivity");
}
private PendingIntent buildFixedPendingIntent() {
// 固定跳MainActivity不支持自定义目标
Intent intent = new Intent(mContext, MainActivity.class);
// API29+ 强制要求:明确包名,避免跳转目标模糊
intent.setPackage(DEFAULT_JUMP_PACKAGE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 确保跳转生效
LogUtils.d(TAG, "【Intent构建】跳转包名:" + DEFAULT_JUMP_PACKAGE + ",目标:" + intent.getComponent().getClassName());
intent.setPackage(mContext.getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); // 跳转时清除栈顶避免重复创建Activity
LogUtils.d(TAG, "【Intent构建】所有通知统一跳转MainActivity包名" + mContext.getPackageName());
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext,
@@ -200,8 +196,8 @@ public class NotificationManagerUtils {
return;
}
// 1. 构建跳转Intent
PendingIntent pendingIntent = buildPendingIntent(null); // 默认跳MainActivity
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知兼容API26+渠道低版本用Builder
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -282,8 +278,8 @@ public class NotificationManagerUtils {
LogUtils.e(TAG, "【电量提醒通知】初始化失败Service/NotificationMessage为空");
return;
}
// 1. 构建跳转Intent
PendingIntent pendingIntent = buildPendingIntent(null);
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -337,19 +333,18 @@ public class NotificationManagerUtils {
// ====================== 场景3通用临时通知简单文本自动取消无需自定义布局======================
/**
* 显示通用临时通知(普通告警,仅震动,自动取消,支持自定义跳转目标
* @param targetIntent 自定义跳转Intent传null默认跳MainActivity
* 显示通用临时通知(普通告警,仅震动,自动取消,统一跳转MainActivity
* @param title 通知标题
* @param content 通知内容
*/
public void showTempAlertNotify(Intent targetIntent, String title, String content) {
public void showTempAlertNotify(String title, String content) {
LogUtils.d(TAG, "【通用临时通知】开始构建,标题:" + title + ",内容:" + content);
if (title == null || content == null) {
LogUtils.e(TAG, "【通用临时通知】构建失败:标题/内容为空");
return;
}
// 1. 构建跳转Intent
PendingIntent pendingIntent = buildPendingIntent(targetIntent);
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 用NotificationCompat.Builder兼容所有版本简化逻辑
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher)
@@ -369,19 +364,18 @@ public class NotificationManagerUtils {
// ====================== 场景4自定义布局通知灵活扩展支持复杂样式======================
/**
* 显示自定义布局通知(支持普通布局+大布局,通用所有场景,可自定义跳转
* @param targetIntent 自定义跳转Intent
* 显示自定义布局通知(支持普通布局+大布局,通用所有场景,统一跳转MainActivity
* @param contentView 普通自定义布局(必填)
* @param bigContentView 下拉大布局(可选)
*/
public void showCustomLayoutNotify(Intent targetIntent, RemoteViews contentView, RemoteViews bigContentView) {
public void showCustomLayoutNotify(RemoteViews contentView, RemoteViews bigContentView) {
LogUtils.d(TAG, "【自定义布局通知】开始构建布局ID" + (contentView != null ? contentView.getLayoutId() : null));
if (contentView == null) {
LogUtils.e(TAG, "【自定义布局通知】构建失败普通布局contentView为空");
return;
}
// 1. 构建跳转Intent
PendingIntent pendingIntent = buildPendingIntent(targetIntent);
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建自定义布局通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher) // 必传,不可省略

View File

@@ -1,32 +1,35 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.os.PowerManager;
import android.provider.Settings;
import cc.winboll.studio.libappbase.LogUtils;
/**
* 权限申请工具类
* 适配 Android 13+ 媒体权限 & 低版本存储权限
* 兼容 SDK 版本低于 33 的编译环境
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/14 03:05
* @Describe 权限申请工具类Java7兼容版
* 适配 小米手机+API30专注自启动权限、电池优化权限检查与申请
*/
public class PermissionUtils {
private static final String TAG = "PermissionUtils";
// 存储权限请求码
public static final int REQUEST_STORAGE = 1000;
// 媒体图片权限请求码Android 13+
public static final int REQUEST_READ_MEDIA_IMAGES = 1001;
// ====================== 常量定义(统一管理,首屏可见)======================
public static final String TAG = "PermissionUtils";
// 权限请求码(仅保留核心权限场景)
public static final int REQUEST_IGNORE_BATTERY_OPTIMIZATION = 1000; // 电池优化权限
public static final int REQUEST_AUTO_START = 1001; // 自启动权限(小米专属)
// SDK版本常量适配API30替代系统枚举
private static final int SDK_VERSION_R = 30; // Android 11API30
// 小米手机自启动权限页面包名/类名(小米专属跳转路径)
private static final String XIAOMI_AUTO_START_PACKAGE = "com.miui.securitycenter";
private static final String XIAOMI_AUTO_START_CLASS = "com.miui.permcenter.autostart.AutoStartManagementActivity";
// 手动定义 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;
// 单例模式
// ====================== 单例模式Java7标准双重校验锁======================
private static volatile PermissionUtils sInstance;
private PermissionUtils() {}
@@ -36,99 +39,184 @@ public class PermissionUtils {
synchronized (PermissionUtils.class) {
if (sInstance == null) {
sInstance = new PermissionUtils();
LogUtils.d(TAG, "【单例初始化】PermissionUtils 实例创建成功");
}
}
}
return sInstance;
}
// ====================== 自启动权限(拆分检查+请求,小米专属)======================
/**
* 检查并请求 存储权限Android 12及以下
* 检查是否拥有自启动权限小米手机专属判断API30适配
* 注小米自启动无系统API直接校验通过「是否为小米机型」+「功能场景间接判断」,此处返回机型适配状态
* @param activity 上下文Activity不可为null
* @return true小米机型需手动开启权限false非小米机型无需申请
*/
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;
}
public boolean checkAutoStartPermission(Activity activity) {
if (activity == null) {
LogUtils.e(TAG, "【自启动权限-检查】失败Activity为空");
return false;
}
return true;
LogUtils.d(TAG, "【自启动权限-检查】开始,设备品牌:" + Build.BRAND + ",系统版本:" + Build.VERSION.SDK_INT);
// 仅小米机型需要申请自启动权限非小米直接返回false无需处理
boolean isXiaomi = Build.BRAND.toLowerCase().contains("xiaomi");
LogUtils.d(TAG, "【自启动权限-检查】结果:" + (isXiaomi ? "小米机型(需手动开启)" : "非小米机型(无需申请)"));
return isXiaomi;
}
/**
* 检查并请求 媒体图片权限Android 13+
* 兼容 SDK 编译版本低于 33 的情况
* 请求自启动权限小米手机专属API30适配跳转系统页面引导开启
* @param activity 申请权限的Activity不可为null
*/
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;
}
public void requestAutoStartPermission(Activity activity) {
if (activity == null) {
LogUtils.e(TAG, "【自启动权限-请求】失败Activity为空");
return;
}
// 低版本已通过存储权限覆盖直接返回true
return true;
}
/**
* 权限请求结果处理
*/
public void handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_STORAGE:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "存储权限申请成功");
} else {
LogUtils.e(TAG, "存储权限申请失败");
}
break;
case REQUEST_READ_MEDIA_IMAGES:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
LogUtils.d(TAG, "媒体图片权限申请成功");
} else {
LogUtils.e(TAG, "媒体图片权限申请失败");
}
break;
default:
LogUtils.d(TAG, "未知权限请求码:" + requestCode);
// 先检查机型,非小米不执行请求
if (!checkAutoStartPermission(activity)) {
LogUtils.d(TAG, "【自启动权限-请求】非小米机型,无需发起请求");
return;
}
}
LogUtils.d(TAG, "【自启动权限-请求】开始处理,系统版本:" + Build.VERSION.SDK_INT);
/**
* 检查是否有管理所有文件权限Android 11+
*/
public boolean checkManageExternalStoragePermission(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return android.os.Environment.isExternalStorageManager();
}
return true;
}
/**
* 请求管理所有文件权限Android 11+
*/
public void requestManageExternalStoragePermission(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// API30+ 小米手机:优先精准跳转自启动管理页
if (Build.VERSION.SDK_INT >= SDK_VERSION_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());
// 方案1组件名精准跳转成功率最高
Intent intent = new Intent();
intent.setComponent(new ComponentName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS));
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "【自启动权限-请求】跳转小米自启动管理页(组件名跳转)");
} catch (Exception e1) {
try {
// 方案2Action备用跳转兼容机型差异
Intent intent = new Intent("miui.intent.action.OP_AUTO_START");
intent.setClassName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "【自启动权限-请求】跳转小米自启动管理页Action跳转");
} catch (Exception e2) {
// 方案3终极备用跳转系统设置引导手动操作
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.w(TAG, "【自启动权限-请求】跳转系统设置页(引导手动开启)");
showAutoStartTipsDialog(activity);
}
}
return;
}
// API30以下小米手机兼容低版本跳转逻辑
LogUtils.d(TAG, "【自启动权限-请求】API30以下小米机型执行低版本跳转");
try {
Intent intent = new Intent(XIAOMI_AUTO_START_CLASS);
intent.setPackage(XIAOMI_AUTO_START_PACKAGE);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
} catch (Exception e) {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
showAutoStartTipsDialog(activity);
}
}
// ====================== 电池优化权限(拆分检查+请求,通用所有机型)======================
/**
* 检查是否拥有「忽略电池优化」权限API30适配通用所有机型精准返回权限状态
* @param activity 上下文Activity不可为null
* @return true已拥有忽略优化false未拥有需申请
*/
public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) {
if (activity == null) {
LogUtils.e(TAG, "【电池优化权限-检查】失败Activity为空");
return false;
}
LogUtils.d(TAG, "【电池优化权限-检查】开始,系统版本:" + Build.VERSION.SDK_INT);
// API23以下无电池优化权限直接视为已拥有
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
LogUtils.d(TAG, "【电池优化权限-检查】API23以下无此权限视为已拥有");
return true;
}
// API23+ 精准校验权限状态
PowerManager powerManager = (PowerManager) activity.getSystemService(Activity.POWER_SERVICE);
if (powerManager == null) {
LogUtils.e(TAG, "【电池优化权限-检查】获取PowerManager失败无法校验");
return false;
}
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());
LogUtils.d(TAG, "【电池优化权限-检查】结果:" + (isIgnored ? "已拥有(忽略优化)" : "未拥有(需申请)"));
return isIgnored;
}
/**
* 请求「忽略电池优化」权限API30适配通用所有机型自动判断是否需要申请
* @param activity 申请权限的Activity不可为null
*/
public void requestIgnoreBatteryOptimizationPermission(Activity activity) {
if (activity == null) {
LogUtils.e(TAG, "【电池优化权限-请求】失败Activity为空");
return;
}
// 先检查权限,已拥有则直接返回
if (checkIgnoreBatteryOptimizationPermission(activity)) {
LogUtils.d(TAG, "【电池优化权限-请求】已拥有权限,无需发起申请");
return;
}
LogUtils.w(TAG, "【电池优化权限-请求】未拥有权限,开始发起申请");
try {
// 方案1直接跳转权限申请页用户一键同意优先使用
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.d(TAG, "【电池优化权限-请求】跳转系统权限申请页");
} catch (Exception e) {
// 方案2备用跳转跳转优化管理列表引导手动选择
Intent intent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.w(TAG, "【电池优化权限-请求】跳转优化管理页(引导手动开启)");
showBatteryOptTipsDialog(activity);
}
}
// ====================== 辅助方法(提示弹窗+结果处理)======================
/**
* 显示自启动权限手动开启提示弹窗(小米机型跳转失败时使用)
*/
private void showAutoStartTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("权限申请提示")
.setMessage("请手动开启自启动权限,否则应用后台功能可能异常:\n1. 进入安全中心 → 应用管理 → 自启动管理\n2. 找到本应用,开启「允许自启动」开关")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
LogUtils.d(TAG, "【自启动权限】显示手动开启提示弹窗");
}
/**
* 显示电池优化权限手动开启提示弹窗(跳转失败时使用)
*/
private void showBatteryOptTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity)
.setTitle("权限申请提示")
.setMessage("请手动忽略电池优化,否则应用后台运行可能被限制:\n1. 进入设置 → 电池 → 电池优化\n2. 找到本应用,选择「不优化」")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
LogUtils.d(TAG, "【电池优化权限】显示手动开启提示弹窗");
}
}

View File

@@ -20,8 +20,7 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF28C000">
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
@@ -32,7 +31,6 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF3243E2"
android:id="@+id/background_view">
</cc.winboll.studio.powerbell.views.BackgroundView>
@@ -42,8 +40,7 @@
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#B92FABE6">
android:layout_height="400dp">
<RelativeLayout
android:layout_width="match_parent"