Compare commits

...

42 Commits

Author SHA1 Message Date
493b7e433c <powerbell>APK 15.14.9 release Publish. 2025-12-16 21:18:25 +08:00
d2ddfedc96 调整当前电量初始化时的默认值。处理bean类序列化问题,未调试。 2025-12-16 21:16:34 +08:00
bba48a4458 <powerbell>APK 15.14.8 release Publish. 2025-12-16 20:17:03 +08:00
b7ae6ce190 编译参数修复 2025-12-16 20:15:58 +08:00
ba5470ebcb 更新像素清空按钮的重置颜色值。重置后为黑色,透明度OriginalAlpha值为FF。 2025-12-16 20:14:23 +08:00
78c7763212 优化颜色拾取流程 2025-12-16 20:06:12 +08:00
d051b1f737 <powerbell>APK 15.14.7 release Publish. 2025-12-16 17:52:54 +08:00
d6323bc1ed <powerbell>Start New Stage Version. 2025-12-16 17:50:40 +08:00
dffcc0f8a0 渐变像素对话框调试完成。 2025-12-16 17:48:59 +08:00
9426618b59 20251216_171108_907 2025-12-16 17:11:20 +08:00
68d98d4be3 调色板基本调试完成 2025-12-16 16:24:09 +08:00
4db458dda8 20251216_154557_434 2025-12-16 15:46:01 +08:00
83a8f5dada 初步完成调色板调用流程调试。 2025-12-16 14:12:41 +08:00
8e1d6ba197 添加调色板对话框,未调试。 2025-12-16 12:11:32 +08:00
70a004d9e3 <powerbell>APK 15.14.6 release Publish. 2025-12-14 19:58:14 +08:00
c7f8aea1ce <powerbell>Start New Stage Version. 2025-12-14 19:57:39 +08:00
6d4381d78a 修复固定剪裁时的宽高比例不准确的BUG。 2025-12-14 19:56:36 +08:00
ddcd9a450e 20251214_191909_625 2025-12-14 19:19:13 +08:00
ca2323f534 <powerbell>APK 15.14.5 release Publish. 2025-12-14 18:30:51 +08:00
851800e39a 背景图片第二次无法更换的Bug修复。 2025-12-14 18:29:44 +08:00
f17624048c <powerbell>APK 15.14.4 release Publish. 2025-12-14 18:12:02 +08:00
724fce895f 减去多余应用图标。 2025-12-14 18:10:40 +08:00
5ece532dd4 <powerbell>APK 15.14.3 release Publish. 2025-12-14 17:48:59 +08:00
8b20bc84c8 更新测试数据 2025-12-14 17:47:02 +08:00
634c71dfd4 剪裁图片透明度问题解决 2025-12-14 17:42:12 +08:00
947df2e9b4 20251214_165544_910 2025-12-14 16:55:59 +08:00
08a33365b3 <powerbell>APK 15.14.2 release Publish. 2025-12-14 04:55:45 +08:00
7cffe5c0a5 <powerbell>Start New Stage Version. 2025-12-14 04:54:24 +08:00
5a0c429131 背景像素切换流程测试完成 2025-12-14 04:53:17 +08:00
cff26b3d11 背景像素拾取功能测试完成 2025-12-14 04:46:17 +08:00
e59034e48d 添加权限申请提示框 2025-12-14 04:18:29 +08:00
3d3301064c 必要权限申请设置完成。 2025-12-14 04:08:10 +08:00
2d12397f5e 减少初始化应用相册目录时的多余目录。 2025-12-14 02:28:53 +08:00
f09bb17cbc 修改所有通知栏,点击后都跳转到主窗口。 2025-12-14 02:10:13 +08:00
28d8a5679f <powerbell>APK 15.14.1 release Publish. 2025-12-13 21:16:25 +08:00
b4d9bdf3b3 <powerbell>APK 15.14.0 release Publish. 2025-12-13 21:15:48 +08:00
111cf01f9a <powerbell>Start New Stage Version. 2025-12-13 21:14:54 +08:00
e51d46186a 数据模型有调整,设定次级版本号。同次级版本应用可以自由切换。 2025-12-13 21:11:03 +08:00
8fc6855066 通知类调试完成 2025-12-13 21:04:35 +08:00
4ceaf1e46a 通知类重构 2025-12-13 20:47:00 +08:00
e669bbb04b 命名空间重构 2025-12-13 20:23:48 +08:00
6bf3ebe2fd 20251212_002702_716 2025-12-12 00:27:07 +08:00
56 changed files with 3459 additions and 1553 deletions

View File

@@ -29,11 +29,11 @@ android {
applicationId "cc.winboll.studio.powerbell"
minSdkVersion 23
targetSdkVersion 30
versionCode 6
versionCode 7
// versionName 更新后需要手动设置
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
versionName "15.12"
versionName "15.14"
if(true) {
versionName = genVersionName("${versionName}")
}

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Thu Dec 11 20:54:28 HKT 2025
stageCount=16
#Tue Dec 16 21:18:25 HKT 2025
stageCount=10
libraryProject=
baseVersion=15.12
publishVersion=15.12.15
baseVersion=15.14
publishVersion=15.14.9
buildCount=0
baseBetaVersion=15.12.16
baseBetaVersion=15.14.10

View File

@@ -4,55 +4,56 @@
xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell">
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 拍摄照片和视频 -->
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 运行前台服务 -->
<!-- ====================== 原有权限保留 + 补充核心权限 ====================== -->
<!-- 运行前台服务(原有保留,补充 Android 12+ 特殊前台服务权限) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- 读取您共享存储空间中的内容 -->
<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"/>
<!-- PACKAGE_USAGE_STATS -->
<!-- 应用使用统计相关(原有保留,忽略保护权限警告) -->
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<!-- BATTERY_STATS -->
<uses-permission android:name="android.permission.BATTERY_STATS"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<!-- 计算应用存储空间 -->
<!-- 相机相关(原有保留,补充权限等级声明,避免安装警告) -->
<uses-feature
android:name="android.hardware.camera"
android:required="false"/> <!-- 非核心功能设为非必须,兼容无相机设备 -->
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false"/>
<uses-permission android:name="android.permission.CAMERA" /> <!-- 补充相机权限原有仅声明feature无权限 -->
<!-- 应用信息相关(原有保留) -->
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<!-- 新增:文件管理权限(对应 PermissionUtils 全文件管理逻辑) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- API30+ 全文件权限 -->
<!-- 新增:忽略电池优化权限(对应 PermissionUtils 电池优化逻辑,必须声明) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- 新增API30+ 跳转系统权限页兼容(避免自启/文件权限跳转失败) -->
<queries>
<!-- 全文件管理权限跳转兼容 -->
<!-- 小米自启权限跳转兼容(精准匹配安全中心包名) -->
<package android:name="com.miui.securitycenter" />
<!-- 电池优化权限跳转兼容 -->
</queries>
<application
android:name=".App"
@@ -64,17 +65,20 @@
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning">
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<!-- ====================== 页面配置(原有保留,优化 exported 安全) ====================== -->
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:launchMode="singleTask">
</activity>
<activity android:name=".activities.CrashActivity"/>
<activity
android:name=".activities.CrashActivity"
android:exported="false"/> <!-- 新增:非外部调用,设为 false提升安全 -->
<activity-alias
android:name=".MainActivityEN1"
@@ -83,19 +87,13 @@
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/>
</activity-alias>
<activity-alias
@@ -105,19 +103,13 @@
android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/>
</activity-alias>
<activity-alias
@@ -127,116 +119,121 @@
android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher"
android:enabled="false">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/>
</activity-alias>
<activity
android:name="cc.winboll.studio.powerbell.activities.ClearRecordActivity"
android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask">
</activity>
android:launchMode="singleTask"
android:exported="false"/> <!-- 新增:非外部调用,设为 false -->
<activity
android:name="cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity"
android:name=".activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<!-- ====================== 广播接收器(优化自启广播,提升保活成功率) ====================== -->
<receiver
android:name=".receivers.MainReceiver"
android:enabled="true"
android:exported="false"
android:exported="true"
android:directBootAware="true">
<intent-filter>
<intent-filter android:priority="1000"> <!-- 新增:广播优先级最高,确保优先接收开机广播 -->
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<!-- 补充:充电/解锁广播,增强后台唤醒机会 -->
<action android:name="android.intent.action.POWER_CONNECTED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter>
</receiver>
<!-- ====================== 服务配置(核心优化:前台服务保活,适配 API29-30 ====================== -->
<!-- 核心前台服务ControlCenterService保活核心重点优化 -->
<service
android:name="cc.winboll.studio.powerbell.services.ControlCenterService"
android:name=".services.ControlCenterService"
android:priority="1000"
android:enabled="true"
android:exported="false"
android:process=".controlcenterservice"/>
android:process=".controlcenterservice"
android:foregroundServiceType="dataSync">
<!-- 新增Android 12+ 前台服务用途声明(系统强制,否则拦截服务启动) -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="后台核心功能运行、持续保活" /> <!-- 按实际用途填写,不可空 -->
</service>
<!-- 辅助服务AssistantService按需优化增强稳定性 -->
<service
android:name="cc.winboll.studio.powerbell.services.AssistantService"
android:name=".services.AssistantService"
android:enabled="true"
android:exported="false"
android:process=".assistantservice"/>
android:process=".assistantservice"> <!-- 若需前台启动,添加此配置;纯后台可移除 -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行" />
</service>
<!-- ====================== 其他配置(原有保留,补充优化) ====================== -->
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReporterActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
<activity android:name="cc.winboll.studio.powerbell.unittest.MainUnitTestActivity"/>
<!-- 所有非外部调用的 Activity统一设 exported=false提升安全 -->
<activity
android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity
android:name=".activities.PixelPickerActivity"
android:exported="false"/>
<activity
android:name=".activities.BatteryReportActivity"
android:exported="false"/>
<activity
android:name=".unittest.MainUnitTestActivity"
android:exported="false"/>
<activity
android:name=".activities.ShortcutActionActivity"
android:exported="false"/>
<activity
android:name=".activities.SettingsActivity"
android:exported="false"/>
<!-- 文件提供者(原有保留,正常使用) -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/>
</provider>
<activity android:name="cc.winboll.studio.powerbell.activities.ShortcutActionActivity"/>
<activity android:name="cc.winboll.studio.powerbell.activities.SettingsActivity"/>
<!-- 1. 注册 UCropActivity关键解决崩溃 -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"> <!-- 必须添加Android 12+ 要求显式声明 exported -->
</activity>
<!-- UCrop 第三方页面原有保留exported=true 正常) -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true">
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -8,13 +8,12 @@ import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver;
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,19 +25,21 @@ 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;
import cc.winboll.studio.powerbell.activities.SettingsActivity;
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
@@ -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,13 @@ 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);
permissionUtils.startPermissionRequest(this);
}
@Override
protected void onDestroy() {
@@ -189,7 +192,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 +212,16 @@ 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) {
permissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
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() {
@@ -298,7 +297,7 @@ public class MainActivity extends WinBoLLActivity {
if (mViewHolder == null || mAppConfigUtils == null) return;
int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue();
int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue();
int nUsegeReminderValue = mAppConfigUtils.getUsageReminderValue();
int nCurrentValue = mAppConfigUtils.getCurrentValue();
if (mViewHolder.llLeftSeekBar != null && mViewHolder.llRightSeekBar != null && mDrawableFrame != null) {
@@ -328,7 +327,7 @@ public class MainActivity extends WinBoLLActivity {
mViewHolder.chargeReminderSeekBar.setProgress(nChargeReminderValue);
}
if (mViewHolder.cbIsEnableChargeReminder != null) {
mViewHolder.cbIsEnableChargeReminder.setChecked(mAppConfigUtils.getIsEnableChargeReminder());
mViewHolder.cbIsEnableChargeReminder.setChecked(mAppConfigUtils.isEnableChargeReminder());
}
if (mViewHolder.tvUsegeReminderValue != null) {
@@ -339,7 +338,7 @@ public class MainActivity extends WinBoLLActivity {
mViewHolder.usegeReminderSeekBar.setProgress(nUsegeReminderValue);
}
if (mViewHolder.cbIsEnableUsegeReminder != null) {
mViewHolder.cbIsEnableUsegeReminder.setChecked(mAppConfigUtils.getIsEnableUsegeReminder());
mViewHolder.cbIsEnableUsegeReminder.setChecked(mAppConfigUtils.isEnableUsageReminder());
}
if (mViewHolder.tvCurrentValue != null) {
@@ -369,7 +368,7 @@ public class MainActivity extends WinBoLLActivity {
mViewHolder.cbIsEnableChargeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAppConfigUtils.setIsEnableChargeReminder(mViewHolder.cbIsEnableChargeReminder.isChecked());
mAppConfigUtils.setEnableChargeReminder(mViewHolder.cbIsEnableChargeReminder.isChecked());
}
});
}
@@ -381,7 +380,7 @@ public class MainActivity extends WinBoLLActivity {
mViewHolder.cbIsEnableUsegeReminder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAppConfigUtils.setIsEnableUsegeReminder(mViewHolder.cbIsEnableUsegeReminder.isChecked());
mAppConfigUtils.setEnableUsageReminder(mViewHolder.cbIsEnableUsegeReminder.isChecked());
}
});
}
@@ -590,7 +589,7 @@ public class MainActivity extends WinBoLLActivity {
public void onStopTrackingTouch(SeekBar seekBar) {
if (mAppConfigUtils == null || mViewHolder == null || mViewHolder.tvUsegeReminderValue == null) return;
int nUsegeReminderValue = ((VerticalSeekBar) seekBar)._mnProgress;
mAppConfigUtils.setUsegeReminderValue(nUsegeReminderValue);
mAppConfigUtils.setUsageReminderValue(nUsegeReminderValue);
mViewHolder.tvUsegeReminderValue.setText(String.valueOf(nUsegeReminderValue) + "%");
}
}

View File

@@ -23,22 +23,23 @@ import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog;
import cc.winboll.studio.powerbell.dialogs.ColorPaletteDialog;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
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.UriUtil;
import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener {
public class BackgroundSettingsActivity extends WinBoLLActivity {
// ====================== 常量定义 ======================
public static final String TAG = "BackgroundSettingsActivity";
@@ -47,11 +48,10 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
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 implements Backg
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 implements Backg
}
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 implements Backg
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 implements Backg
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 implements Backg
}
}
@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();
}
@@ -221,6 +208,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
findViewById(R.id.activitybackgroundsettingsAButton1).setOnClickListener(onColorPaletteClickListener);
}
// ====================== 按钮点击事件 ======================
@@ -238,15 +226,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
@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 +276,17 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
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 +299,11 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
};
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 +312,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
}
Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class);
intent.putExtra("imagePath", targetImagePath);
startActivity(intent);
startActivityForResult(intent, REQUEST_PIXELPICKER);
LogUtils.d(TAG, "【像素拾取启动】路径:" + targetImagePath);
}
};
@@ -345,16 +321,43 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】清空像素颜色");
BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean();
BackgroundBean bean = mBgSourceUtils.getPreviewBackgroundBean();
int oldColor = bean.getPixelColor();
bean.setPixelColor(0);
bean.setPixelColor(0xFF000000);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
ToastUtils.show("像素颜色已清空");
LogUtils.d(TAG, "【像素清空】旧颜色:" + oldColor);
}
};
private View.OnClickListener onColorPaletteClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "【按钮点击】调色板按钮");
// 初始颜色(白色,含透明度)
//int initialColor = 0xFFFFFFFF;
int initialColor = mBgSourceUtils.getPreviewBackgroundBean().getPixelColor();
// 显示对话框
ColorPaletteDialog dialog = new ColorPaletteDialog(BackgroundSettingsActivity.this, initialColor, new ColorPaletteDialog.OnColorSelectedListener() {
@Override
public void onColorSelected(int color) {
// 回调返回 0xAARRGGBB 格式颜色,直接使用
mBgSourceUtils.getPreviewBackgroundBean().setPixelColor(color);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
LogUtils.d("选择颜色", String.format("#%08X", color));
}
});
dialog.show();
LogUtils.d(TAG, "调色板按钮响应完成。");
}
};
// ====================== 工具方法 ======================
/**
* 生成 FileProvider Uri适配 Android 7.0+
@@ -395,6 +398,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
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 +413,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
try {
mBgSourceUtils.loadSettings();
mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean(), true);
mBackgroundView.setBackgroundColor(mBgSourceUtils.getPreviewBackgroundBean().getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
@@ -573,7 +578,12 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) {
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this);
BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this, new BackgroundPicturePreviewDialog.IOnRecivedPictureListener(){
@Override
public void onAcceptRecivedPicture(Uri uriRecivedPicture) {
ToastUtils.show(String.format("uriRecivedPicture %s", uriRecivedPicture));
}
});
dlg.show();
LogUtils.d(TAG, "【分享处理】收到分享图片意图");
return true;
@@ -802,7 +812,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
* 将 Uri 文件同步到预览 Bean
*/
boolean putUriFileToPreviewSource(Uri srcUriFile) {
String filePath = UriUtil.getFilePathFromUri(this, srcUriFile);
String filePath = UriUtils.getFilePathFromUri(this, srcUriFile);
if (TextUtils.isEmpty(filePath)) {
LogUtils.e(TAG, "putUriFileToPreviewSource: Uri解析路径为空");
return false;
@@ -847,41 +857,40 @@ public class BackgroundSettingsActivity extends WinBoLLActivity implements Backg
previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings();
doubleRefreshPreview();
float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
if (systemFileRatio > 0) {
Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
if (isBitmapValid(cropBitmap)) {
Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
if (isBitmapValid(scaledCropBitmap)) {
saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
scaledCropBitmap.recycle();
}
cropBitmap.recycle();
} else {
LogUtils.e(TAG, "【裁剪结果】裁剪Bitmap解析无效");
}
}
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (!isFinishing()) {
doubleRefreshPreview();
LogUtils.d(TAG, "【裁剪结果】触发双重刷新");
}
}
}, 300);
// float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
// if (systemFileRatio > 0) {
// Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
// if (isBitmapValid(cropBitmap)) {
// Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
// if (isBitmapValid(scaledCropBitmap)) {
// saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
// scaledCropBitmap.recycle();
// }
// cropBitmap.recycle();
// } else {
// LogUtils.e(TAG, "【裁剪结果】裁剪Bitmap解析无效");
// }
// }
//
// new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
// @Override
// public void run() {
// if (!isFinishing()) {
// doubleRefreshPreview();
// LogUtils.d(TAG, "【裁剪结果】触发双重刷新");
// }
// }
// }, 300);
} else {
handleOperationCancelOrFail();
}
}
// ====================== 接口实现 ======================
@Override
public void onAcceptRecivedPicture(String szPreRecivedPictureName) {
ToastUtils.show("图片接收功能暂未实现");
LogUtils.d(TAG, "【分享接收】图片名:" + szPreRecivedPictureName);
}
private void handlePixelPickerResult(int requestCode, int resultCode, Intent data) {
doubleRefreshPreview();
isPreviewBackgroundChanged = true;
}
}

View File

@@ -12,7 +12,7 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;

View File

@@ -26,7 +26,7 @@ import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import java.io.File;
import java.io.FileInputStream;
@@ -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

@@ -12,7 +12,7 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
import cc.winboll.studio.powerbell.model.BatteryData;
import cc.winboll.studio.powerbell.models.BatteryData;
import java.util.ArrayList;
import java.util.List;

View File

@@ -2,22 +2,19 @@ package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.UriUtil;
import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File;
import java.io.IOException;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
@@ -29,21 +26,25 @@ public class BackgroundPicturePreviewDialog extends Dialog {
public static final String TAG = "BackgroundPicturePreviewDialog";
Context mContext;
BackgroundSourceUtils mBackgroundPictureUtils;
//BackgroundSourceUtils mBackgroundPictureUtils;
Button dialogbackgroundpicturepreviewButton1;
Button dialogbackgroundpicturepreviewButton2;
String mszPreReceivedFileName;
//String mszPreReceivedFileName;
IOnRecivedPictureListener mIOnRecivedPictureListener;
Uri mUriRecivedPicture;
BackgroundView mBackgroundView;
public BackgroundPicturePreviewDialog(Context context) {
public BackgroundPicturePreviewDialog(Context context, IOnRecivedPictureListener iOnRecivedPictureListener) {
super(context);
setContentView(R.layout.dialog_backgroundpicturepreview);
initEnv();
mIOnRecivedPictureListener = iOnRecivedPictureListener;
//initEnv();
mContext = context;
mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
//mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext);
ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1);
copyAndViewRecivePicture(imageView);
mBackgroundView = findViewById(R.id.backgroundview);
previewRecivedPicture();
dialogbackgroundpicturepreviewButton1 = findViewById(R.id.dialogbackgroundpicturepreviewButton1);
dialogbackgroundpicturepreviewButton1.setOnClickListener(new View.OnClickListener() {
@@ -53,6 +54,7 @@ public class BackgroundPicturePreviewDialog extends Dialog {
// 跳转到主窗口
Intent i = new Intent(mContext, MainActivity.class);
mContext.startActivity(i);
dismiss();
}
});
@@ -62,79 +64,77 @@ public class BackgroundPicturePreviewDialog extends Dialog {
@Override
public void onClick(View v) {
// 使用分享到的图片
//
//LogUtils.d(TAG, "mszReceivedFileName : " + mszReceivedFileName);
((IOnRecivedPictureListener)mContext).onAcceptRecivedPicture(mszPreReceivedFileName);
mIOnRecivedPictureListener.onAcceptRecivedPicture(mUriRecivedPicture);
// 关闭对话框
dismiss();
}
});
}
void initEnv() {
LogUtils.d(TAG, "initEnv()");
mszPreReceivedFileName = "PreReceived.data";
}
// void initEnv() {
// LogUtils.d(TAG, "initEnv()");
// mszPreReceivedFileName = "PreReceived.data";
// }
void copyAndViewRecivePicture(ImageView imageView) {
//AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext());
void previewRecivedPicture() {
BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext);
//取出文件uri
Uri uri = activity.getIntent().getData();
if (uri == null) {
uri = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
mUriRecivedPicture = activity.getIntent().getData();
if (mUriRecivedPicture == null) {
mUriRecivedPicture = activity.getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
}
//获取文件真实地址
String szSrcImage = UriUtil.getFilePathFromUri(mContext, uri);
String szSrcImage = UriUtils.getFilePathFromUri(mContext, mUriRecivedPicture);
if (TextUtils.isEmpty(szSrcImage)) {
Toast.makeText(mContext, "接收到的文件为空。", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
File fSrcImage = new File(szSrcImage);
//mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
// 复制源图片到剪裁文件
try {
FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
LogUtils.d(TAG, "copyFileUsingFileChannels");
Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
imageView.setBackground(drawable);
//LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
mBackgroundView.loadImage(szSrcImage);
//
// File fSrcImage = new File(szSrcImage);
// //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName();
// File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName);
// // 复制源图片到剪裁文件
// try {
// FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto);
// LogUtils.d(TAG, "copyFileUsingFileChannels");
// Drawable drawable = Drawable.createFromPath(mfPreReceivedPhoto.getPath());
// imageView.setBackground(drawable);
// //LogUtils.d(TAG, "mszPreReceivedFileName : " + mszPreReceivedFileName);
// } catch (IOException e) {
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
// }
}
//
// 创建图片背景图片目录
//
boolean createBackgroundFolder2(String szBackgroundFolder) {
// 文件路径参数为空值或无效值时返回false.
if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
return false;
}
LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
File f = new File(szBackgroundFolder);
if (f.exists()) {
if (f.isDirectory()) {
return true;
} else {
// 工作路径不是一个目录
LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
return false;
}
} else {
return f.mkdirs();
}
}
// boolean createBackgroundFolder2(String szBackgroundFolder) {
// // 文件路径参数为空值或无效值时返回false.
// if (szBackgroundFolder == null | szBackgroundFolder.equals("")) {
// return false;
// }
//
// LogUtils.d(TAG, "Background Folder Is : " + szBackgroundFolder);
// File f = new File(szBackgroundFolder);
// if (f.exists()) {
// if (f.isDirectory()) {
// return true;
// } else {
// // 工作路径不是一个目录
// LogUtils.d(TAG, "createImageWorkFolder() error : szImageCacheFolder isDirectory return false. -->" + szBackgroundFolder);
// return false;
// }
// } else {
// return f.mkdirs();
// }
// }
public interface IOnRecivedPictureListener {
void onAcceptRecivedPicture(String szBackgroundFileName);
void onAcceptRecivedPicture(Uri uriRecivedPicture);
}
}

View File

@@ -0,0 +1,747 @@
package cc.winboll.studio.powerbell.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.R;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/12/16 11:47
* @Describe 调色板对话框支持颜色拾取、RGB输入、透明度/亮度调节,兼容 API29-30+ 小米机型)
*/
public class ColorPaletteDialog extends Dialog implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
// ====================== 常量定义(首屏可见,统一管理) ======================
public static final String TAG = "ColorPaletteDialog";
private static final int MAX_RGB_VALUE = 255; // RGB分量最大值0-255
private static final int DEFAULT_BRIGHTNESS = 100; // 默认亮度百分比100%,无调节)
private static final int BRIGHTNESS_STEP = 5; // 亮度调节步长每次±5%,精准流畅)
private static final int MIN_BRIGHTNESS = 10; // 亮度最小值10%,避免全黑看不见)
private static final int MAX_BRIGHTNESS = 200; // 亮度最大值200%,避免过曝失真)
private static final int MAX_ALPHA_PERCENT = 100; // 透明度最大值100%=不透明)
private static final int MIN_ALPHA_PERCENT = 0; // 透明度最小值0%=完全透明)
// ====================== 回调接口(紧跟常量,逻辑关联) ======================
public interface OnColorSelectedListener {
void onColorSelected(int color); // 返回0xAARRGGBB格式颜色含透明度
}
// ====================== 成员变量(按优先级排序:核心数据→控件引用) ======================
// 核心数据:原始基准值(用户输入/选择颜色时更新)+ 实时调节值(亮度/透明度变化时更新)
private OnColorSelectedListener mListener; // 颜色选择回调(非空校验)
private int mInitialColor; // 初始颜色(传入的默认颜色)
private int mCurrentColor; // 当前最终颜色(含亮度+透明度调节)
private int mCurrentBrightnessPercent; // 当前亮度百分比10%-200%
// 透明度百分比0-100%,用户直观操作)+ 原始/实时值0-255颜色计算用
private int mOriginalAlphaPercent; // 原始透明度百分比(基准值,用户输入/选色时更新)
private int mCurrentAlphaPercent; // 实时透明度百分比(调节进度条时更新)
private int mOriginalAlpha; // 原始透明度0-255基准值
private int mCurrentAlpha; // 实时透明度0-255计算用
// RGB原始基准值+实时调节值
private int mOriginalR; // 原始R分量基准值用户输入/选色时更新)
private int mOriginalG; // 原始G分量基准值用户输入/选色时更新)
private int mOriginalB; // 原始B分量基准值用户输入/选色时更新)
private int mCurrentR; // 实时R分量亮度调节后同步输入框显示
private int mCurrentG; // 实时G分量亮度调节后同步输入框显示
private int mCurrentB; // 实时B分量亮度调节后同步输入框显示
// 并发控制标记:是否是应用程序自身在更新颜色(避免循环回调/重复触发)
private static volatile boolean isAppSelfUpdatingColor = false;
// 控件引用(新增透明度进度条+文本)
private ImageView ivColorPicker; // 颜色预览拾取框
private ImageView ivColorScaler; // 颜色渐变拾取框
private EditText etR; // R分量输入框显示实时调节值
private EditText etG; // G分量输入框显示实时调节值
private EditText etB; // B分量输入框显示实时调节值
private EditText etColorValue; // 颜色值输入框(#AARRGGBB显示最终值
private SeekBar sbAlpha; // 透明度调节进度条0-100%
private TextView tvAlphaValue; // 透明度数值显示X%
private TextView tvBrightnessMinus;// 亮度减少按钮(-
private TextView tvBrightnessValue;// 亮度数值显示X%,直观易懂)
private TextView tvBrightnessPlus; // 亮度增加按钮(+
private TextView tvConfirm; // 确认按钮
private TextView tvCancel; // 取消按钮
// ====================== 构造方法(初始化核心数据,严格校验) ======================
public ColorPaletteDialog(Context context, int initialColor, OnColorSelectedListener listener) {
super(context, R.style.CustomDialogStyle);
this.mInitialColor = initialColor;
this.mListener = listener;
// 1. 强制回调非空,避免后续空指针(容错)
if (mListener == null) {
throw new IllegalArgumentException("OnColorSelectedListener can not be null!");
}
// 2. 解析初始颜色:原始基准值 = 实时值(初始无调节)
// 透明度初始颜色的alpha0-255转百分比0-100%
this.mOriginalAlpha = Color.alpha(initialColor);
this.mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
this.mCurrentAlpha = mOriginalAlpha;
this.mCurrentAlphaPercent = mOriginalAlphaPercent;
// RGB初始颜色的RGB分量
this.mOriginalR = Color.red(initialColor);
this.mOriginalG = Color.green(initialColor);
this.mOriginalB = Color.blue(initialColor);
this.mCurrentR = mOriginalR;
this.mCurrentG = mOriginalG;
this.mCurrentB = mOriginalB;
// 3. 初始化当前状态默认亮度100%,当前颜色=初始颜色)
this.mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
this.mCurrentColor = initialColor;
LogUtils.d(TAG, "init dialog success | 初始颜色:" + String.format("#%08X", initialColor)
+ " | 原始RGB" + mOriginalR + "," + mOriginalG + "," + mOriginalB
+ " | 原始透明度:" + mOriginalAlphaPercent + "%"
+ " | 初始亮度:" + mCurrentBrightnessPercent + "%");
}
// ====================== 生命周期方法(按执行顺序排列,逻辑清晰) ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); // 隐藏标题栏
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_color_palette, null);
setContentView(view);
// 初始化流程:控件绑定→数据赋值→监听设置→尺寸适配(小米机型优先适配)
initViewBind(view);
initData();
initListener();
adjustDialogSize();
LogUtils.d(TAG, "dialog create complete | 适配小米API29-30机型");
}
@Override
public void dismiss() {
super.dismiss();
// 释放资源,避免内存泄漏(回调引用置空)
mListener = null;
LogUtils.d(TAG, "dialog dismiss | 释放资源完成");
}
// ====================== 初始化核心方法(职责单一,便于维护) ======================
/**
* 控件绑定(新增透明度进度条+文本绑定)
*/
private void initViewBind(View view) {
ivColorPicker = view.findViewById(R.id.iv_color_picker);
ivColorScaler = view.findViewById(R.id.iv_color_scaler);
etR = view.findViewById(R.id.et_r);
etG = view.findViewById(R.id.et_g);
etB = view.findViewById(R.id.et_b);
etColorValue = view.findViewById(R.id.et_color_value);
sbAlpha = view.findViewById(R.id.sb_alpha);
tvAlphaValue = view.findViewById(R.id.tv_alpha_value);
tvBrightnessMinus = view.findViewById(R.id.tv_brightness_minus);
tvBrightnessValue = view.findViewById(R.id.tv_brightness_value);
tvBrightnessPlus = view.findViewById(R.id.tv_brightness_plus);
tvConfirm = view.findViewById(R.id.tv_confirm);
tvCancel = view.findViewById(R.id.tv_cancel);
// 控件非空校验(小米低版本容错,绑定失败直接关闭对话框)
if (ivColorPicker == null || ivColorScaler == null || etR == null || etG == null || etB == null || etColorValue == null
|| sbAlpha == null || tvAlphaValue == null
|| tvBrightnessMinus == null || tvBrightnessValue == null || tvBrightnessPlus == null
|| tvConfirm == null || tvCancel == null) {
LogUtils.e(TAG, "view bind failed | 请检查布局ID是否正确");
dismiss();
return;
}
LogUtils.d(TAG, "view bind complete | 所有控件绑定成功");
}
/**
* 数据初始化(无监听状态下赋值,避免循环回调)
*/
private void initData() {
// 1. 颜色预览(显示当前最终颜色,初始=原始颜色)
ivColorPicker.setBackgroundColor(mCurrentColor);
// 2. RGB输入框显示「实时分量」初始=原始值)
etR.setText(String.valueOf(mCurrentR));
etG.setText(String.valueOf(mCurrentG));
etB.setText(String.valueOf(mCurrentB));
// 3. 颜色值输入框(显示当前最终颜色,格式#AARRGGBB大写更规范
etColorValue.setText(String.format("#%08X", mCurrentColor));
// 4. 透明度控件(进度条+文本,初始=原始透明度)
sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
// 5. 亮度控件显示默认100%,初始化按钮状态)
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
updateBrightnessBtnStatus(); // 禁用边界值按钮初始100%,都可用)
LogUtils.d(TAG, "init data complete | 原始透明度:" + mOriginalAlphaPercent + "%");
}
/**
* 监听初始化(新增透明度进度条监听)
*/
private void initListener() {
// 点击监听(按钮+颜色拾取框)
ivColorPicker.setOnClickListener(this);
ivColorScaler.setOnClickListener(this);
tvConfirm.setOnClickListener(this);
tvCancel.setOnClickListener(this);
tvBrightnessMinus.setOnClickListener(this);
tvBrightnessPlus.setOnClickListener(this);
// 透明度进度条监听
sbAlpha.setOnSeekBarChangeListener(this);
// 输入框监听RGB+颜色值,避免循环同步)
initTextWatcherListener();
LogUtils.d(TAG, "all listener init complete | 监听绑定成功");
}
/**
* 对话框尺寸适配(小米全面屏+软键盘优化,避免输入框被遮挡)
*/
private void adjustDialogSize() {
Window window = getWindow();
if (window != null) {
WindowManager.LayoutParams lp = window.getAttributes();
// 宽度占屏幕80%,高度自适应(适配不同屏幕尺寸)
lp.width = (int) (getContext().getResources().getDisplayMetrics().widthPixels * 0.8);
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
// 软键盘适配:小米虚拟导航栏兼容,避免输入框被遮挡
window.setAttributes(lp);
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
LogUtils.d(TAG, "dialog size adjust complete | 适配全面屏+软键盘");
}
}
// ====================== 监听子方法(细分类型,逻辑清晰) ======================
/**
* 输入框文本监听RGB+颜色值传入触发ID避免循环同步
*/
private void initTextWatcherListener() {
// RGB输入框监听复用方法减少冗余
setEditTextWatcher(etR, R.id.et_r);
setEditTextWatcher(etG, R.id.et_g);
setEditTextWatcher(etB, R.id.et_b);
// 颜色值输入框监听(支持#RRGGBB/#AARRGGBB格式
etColorValue.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
if (!isAppSelfUpdatingColor) {
parseColorFromStr(s.toString().trim(), R.id.et_color_value);
}
}
});
}
// ====================== 透明度进度条监听实现(核心新增) ======================
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// 仅处理用户手动拖动进度条(避免应用自身更新时触发)
if (fromUser && !isAppSelfUpdatingColor) {
updateAlphaBySeekBar(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
/**
* 拖动透明度进度条更新颜色(核心新增逻辑)
*/
private synchronized void updateAlphaBySeekBar(int alphaPercent) {
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
// 更新实时透明度(百分比+0-255值
mCurrentAlphaPercent = alphaPercent;
mCurrentAlpha = percent2Alpha(alphaPercent);
// 重新计算最终颜色(基于当前亮度+新透明度)
calculateBrightnessAndUpdate();
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, "update alpha by seekbar | 透明度:" + mCurrentAlphaPercent + "%");
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
// ====================== 颜色核心逻辑(新增透明度参数,全功能兼容) ======================
/**
* 核心计算基于原始RGB+当前亮度+当前透明度计算实时RGB+最终颜色
* 逻辑亮度百分比→调节系数→原始RGB×系数→限制0-255→拼接透明度→最终颜色
*/
private void calculateBrightnessAndUpdate() {
// 亮度百分比转调节系数10%→0.1100%→1.0200%→2.0
float brightnessFactor = mCurrentBrightnessPercent / 100.0f;
// RGB三个分量同时调节基于原始基准值避免叠加失真限制0-255
mCurrentR = Math.min(Math.max(Math.round(mOriginalR * brightnessFactor), 0), MAX_RGB_VALUE);
mCurrentG = Math.min(Math.max(Math.round(mOriginalG * brightnessFactor), 0), MAX_RGB_VALUE);
mCurrentB = Math.min(Math.max(Math.round(mOriginalB * brightnessFactor), 0), MAX_RGB_VALUE);
// 拼接「实时透明度」+「实时RGB」得到最终颜色0xAARRGGBB
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
}
/**
* 亮度减少每次减5%最低10%,防止过暗)
*/
private void decreaseBrightness() {
changeBrightness(false);
}
/**
* 亮度增加每次加5%最高200%,防止过曝)
*/
private void increaseBrightness() {
changeBrightness(true);
}
/**
* 亮度调节核心方法(统一逻辑,加并发控制,同步所有控件)
*/
private synchronized void changeBrightness(boolean isIncrease) {
// 关键:判断非应用自身更新,才执行调节(避免重复触发/循环)
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
if (isIncrease) {
if (mCurrentBrightnessPercent >= MAX_BRIGHTNESS) return; // 达到最大值,不处理
mCurrentBrightnessPercent += BRIGHTNESS_STEP; // 增加步长
} else {
if (mCurrentBrightnessPercent <= MIN_BRIGHTNESS) return; // 达到最小值,不处理
mCurrentBrightnessPercent -= BRIGHTNESS_STEP; // 减少步长
}
// 计算亮度调节后的实时RGB+最终颜色(含当前透明度)
calculateBrightnessAndUpdate();
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, (isIncrease ? "increase" : "decrease") + " brightness | "
+ "亮度:" + mCurrentBrightnessPercent + "% | 实时RGB" + mCurrentR + "," + mCurrentG + "," + mCurrentB);
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
/**
* 解析颜色字符串(支持#RRGGBB/#AARRGGBB容错处理更新原始基准值+实时值)
* 新增:解析颜色的透明度,同步更新透明度进度条
*/
private void parseColorFromStr(String colorStr, int triggerViewId) {
// 关键:判断非应用自身更新,才执行解析(避免循环回调)
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
if (TextUtils.isEmpty(colorStr)) return;
// 补全#前缀兼容用户输入习惯如直接输AARRGGBB
if (!colorStr.startsWith("#")) {
colorStr = "#" + colorStr;
}
// 格式校验仅支持6位RRGGBB/8位AARRGGBB避免非法格式
if (colorStr.length() != 7 && colorStr.length() != 9) {
LogUtils.e(TAG, "parse color failed | 格式错误(需#RRGGBB/#AARRGGBB输入" + colorStr);
return;
}
// 解析颜色系统API安全可靠
int parsedColor = Color.parseColor(colorStr);
// 更新原始基准值(用户输入颜色,重置基准)
// 透明度解析颜色的alpha0-255转百分比0-100%
mOriginalAlpha = Color.alpha(parsedColor);
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
// RGB解析颜色的RGB分量
mOriginalR = Color.red(parsedColor);
mOriginalG = Color.green(parsedColor);
mOriginalB = Color.blue(parsedColor);
// 更新实时值(原始值=实时值,无调节)
mCurrentAlpha = mOriginalAlpha;
mCurrentAlphaPercent = mOriginalAlphaPercent;
mCurrentR = mOriginalR;
mCurrentG = mOriginalG;
mCurrentB = mOriginalB;
// 重置亮度为100%
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = parsedColor;
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, "parse color success | 解析颜色:" + String.format("#%08X", parsedColor)
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%");
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "parse color failed | 非法颜色格式,输入:" + colorStr, e);
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
/**
* 通过RGB输入框更新颜色用户输入后更新原始基准值+实时值重置亮度为100%
* 新增透明度基准值保持不变仅更新RGB
*/
private synchronized void updateColorByRGB(int triggerViewId) {
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true; // 标记为应用自身更新
try {
// 解析用户输入的RGB值限制0-255非法输入设为0
int inputR = parseInputValue(etR.getText().toString());
int inputG = parseInputValue(etG.getText().toString());
int inputB = parseInputValue(etB.getText().toString());
// 更新原始基准值(用户手动输入,作为新的调节基准)
mOriginalR = inputR;
mOriginalG = inputG;
mOriginalB = inputB;
// 更新实时值(输入值=实时值,无亮度调节)
mCurrentR = inputR;
mCurrentG = inputG;
mCurrentB = inputB;
// 重置亮度为100%(透明度保持当前值不变)
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
// 计算最终颜色(无亮度调节,拼接当前透明度)
mCurrentColor = Color.argb(mCurrentAlpha, mCurrentR, mCurrentG, mCurrentB);
// 同步所有控件
updateAllViews();
LogUtils.d(TAG, "update color by RGB | 新原始RGB" + mOriginalR + "," + mOriginalG + "," + mOriginalB
+ " | 透明度:" + mCurrentAlphaPercent + "% | 重置亮度:" + DEFAULT_BRIGHTNESS + "%");
} catch (Exception e) {
LogUtils.e(TAG, "update color by RGB failed", e);
} finally {
// 直接释放标记,避免卡顿
isAppSelfUpdatingColor = false;
}
}
}
/**
* 核心同步:更新所有控件显示(新增透明度控件同步,统一方法)
*/
private void updateAllViews() {
// 1. 同步颜色预览(显示最终颜色,含透明度+亮度)
ivColorPicker.setBackgroundColor(mCurrentColor);
// 2. 同步RGB输入框显示实时调节值
etR.setText(String.valueOf(mCurrentR));
etG.setText(String.valueOf(mCurrentG));
etB.setText(String.valueOf(mCurrentB));
// 3. 同步颜色值输入框(显示最终颜色,含透明度,格式#AARRGGBB
etColorValue.setText(String.format("#%08X", mCurrentColor));
// 4. 同步透明度控件(进度条+文本,显示实时透明度)
sbAlpha.setProgress(mCurrentAlphaPercent);
tvAlphaValue.setText(mCurrentAlphaPercent + "%");
// 5. 同步亮度控件(数值+按钮状态)
tvBrightnessValue.setText(mCurrentBrightnessPercent + "%");
updateBrightnessBtnStatus();
LogUtils.d(TAG, "sync all views complete | 最终颜色:" + String.format("#%08X", mCurrentColor)
+ " | 实时RGB" + mCurrentR + "," + mCurrentG + "," + mCurrentB
+ " | 透明度:" + mCurrentAlphaPercent + "% | 亮度:" + mCurrentBrightnessPercent + "%");
}
/**
* 更新亮度按钮状态(边界值禁用,提升交互体验)
*/
private void updateBrightnessBtnStatus() {
// 亮度≤10%禁用减号文字变浅灰≥200%:禁用加号(文字变浅灰)
boolean canMinus = mCurrentBrightnessPercent > MIN_BRIGHTNESS;
boolean canPlus = mCurrentBrightnessPercent < MAX_BRIGHTNESS;
tvBrightnessMinus.setEnabled(canMinus);
tvBrightnessPlus.setEnabled(canPlus);
tvBrightnessMinus.setTextColor(canMinus ? Color.BLACK : Color.parseColor("#CCCCCC"));
tvBrightnessPlus.setTextColor(canPlus ? Color.BLACK : Color.parseColor("#CCCCCC"));
}
// ====================== 工具方法(新增透明度转换工具,通用复用) ======================
/**
* 透明度0-255 → 0-100%(颜色计算值转用户直观百分比)
*/
private int alpha2Percent(int alpha) {
return Math.round((float) alpha / MAX_RGB_VALUE * MAX_ALPHA_PERCENT);
}
/**
* 透明度0-100% → 0-255用户操作百分比转颜色计算值
*/
private int percent2Alpha(int percent) {
return Math.round((float) percent / MAX_ALPHA_PERCENT * MAX_RGB_VALUE);
}
/**
* 解析输入值限制0-255非法输入返回0容错处理
*/
private int parseInputValue(String input) {
if (TextUtils.isEmpty(input)) return 0;
try {
int value = Integer.parseInt(input);
return Math.min(Math.max(value, 0), MAX_RGB_VALUE); // 限制范围,避免溢出
} catch (NumberFormatException e) {
LogUtils.e(TAG, "parse input failed | 非法数字,输入:" + input, e);
return 0;
}
}
/**
* RGB输入框监听复用减少冗余代码统一逻辑
*/
private void setEditTextWatcher(EditText editText, final int viewId) {
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
// 关键:判断非应用自身更新,才执行更新(避免循环回调)
if (!isAppSelfUpdatingColor) {
updateColorByRGB(viewId); // 输入变化后更新颜色
}
}
});
}
/**
* dp转px适配小米不同分辨率避免尺寸错乱通用工具
*/
private int dp2px(float dp) {
return (int) (dp * getContext().getResources().getDisplayMetrics().density + 0.5f);
}
/**
* 显示系统颜色选择器兼容API29-30无高版本依赖小米机型适配
* 核心调整:新增「水平滚动容器+颜色排列容器」二级结构内置圆形按钮无额外drawable依赖
*/
private void showSystemColorPicker() {
LogUtils.d(TAG, "show system color picker | 兼容小米API29-30支持横向滚动");
final android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(getContext());
builder.setTitle("选择基础颜色");
// 50种常用颜色按「红→橙→黄→绿→青→蓝→紫→粉→棕→灰→黑白」彩虹光谱顺序排列
final int[] systemColors = {
// 红色系6种深红→大红→浅红→玫红→暗红→橘红
0xFFCC0000, 0xFFFF0000, 0xFFFF6666, 0xFFFF1493, 0xFF8B0000, 0xFFFF4500,
// 橙色系5种深橙→橙→浅橙→橙黄→橘橙
0xFFCC6600, 0xFFFF8800, 0xFFFFAA33, 0xFFFFBB00, 0xFFF5A623,
// 黄色系5种深黄→黄→浅黄→鹅黄→金黄
0xFFCCCC00, 0xFFFFFF00, 0xFFFFEE99, 0xFFFFFACD, 0xFFFFD700,
// 绿色系7种深绿→绿→浅绿→草绿→薄荷绿→翠绿→墨绿
0xFF006600, 0xFF00FF00, 0xFF99FF99, 0xFF66CC66, 0xFF98FB98, 0xFF00FF99, 0xFF003300,
// 青色系5种深青→青→浅青→蓝绿→青绿
0xFF006666, 0xFF00FFFF, 0xFF99FFFF, 0xFF00CCCC, 0xFF40E0D0,
// 蓝色系8种深蓝→藏蓝→蓝→浅蓝→天蓝→宝蓝→湖蓝→靛蓝
0xFF0000CC, 0xFF00008B, 0xFF0000FF, 0xFF6666FF, 0xFF87CEEB, 0xFF0066FF, 0xFF0099FF, 0xFF4B0082,
// 紫色系6种深紫→紫→浅紫→紫罗兰→紫红→蓝紫
0xFF660099, 0xFF8800FF, 0xFFAA99FF, 0xFF9370DB, 0xFFCBC3E3, 0xFF8A2BE2,
// 粉色系5种深粉→粉→浅粉→嫩粉→桃粉
0xFFFF00FF, 0xFFFF99CC, 0xFFFFCCDD, 0xFFFFB6C1, 0xFFFFA5A5,
// 棕色系4种深棕→棕→浅棕→棕黄
0xFF8B4513, 0xFFA0522D, 0xFFD2B48C, 0xFFCD853F,
// 灰色系6种深灰→灰→浅灰→银灰→淡灰→浅银灰
0xFF333333, 0xFF666666, 0xFF888888, 0xFFAAAAAA, 0xFFCCCCCC, 0xFFE6E6E6,
// 黑白系3种黑→白→米白
0xFF000000, 0xFFFFFFFF, 0xFFFFFAFA
};
// 1. 第一级:水平滚动容器
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
horizontalScrollView.setHorizontalScrollBarEnabled(true);
horizontalScrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
horizontalScrollView.setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5));
// 2. 第二级:颜色排列容器(横向)
LinearLayout colorLayout = new LinearLayout(getContext());
colorLayout.setOrientation(LinearLayout.HORIZONTAL);
colorLayout.setGravity(Gravity.CENTER_VERTICAL);
colorLayout.setPadding(dp2px(10), dp2px(10), dp2px(10), dp2px(10));
// 3. 循环添加颜色按钮(内置圆形效果,无额外依赖)
for (int i = 0; i < systemColors.length; i++) {
final int color = systemColors[i];
ImageView colorBtn = new ImageView(getContext());
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(dp2px(40), dp2px(40));
if (i != systemColors.length - 1) {
lp.setMargins(0, 0, dp2px(10), 0); // 按钮间距
}
colorBtn.setLayoutParams(lp);
// 核心:内置圆形背景(白色边框+圆形形状无需drawable文件
GradientDrawable circleBg = new GradientDrawable();
circleBg.setShape(GradientDrawable.OVAL); // 圆形
circleBg.setColor(color); // 按钮颜色
circleBg.setStroke(dp2px(2), Color.WHITE); // 白色边框2dp宽区分颜色
colorBtn.setBackground(circleBg); // 设置圆形背景
colorBtn.setClickable(true);
colorBtn.setFocusable(true);
// 点击事件(逻辑不变)
colorBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true;
try {
mOriginalAlpha = Color.alpha(color);
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
mOriginalR = Color.red(color);
mOriginalG = Color.green(color);
mOriginalB = Color.blue(color);
mCurrentAlpha = mOriginalAlpha;
mCurrentAlphaPercent = mOriginalAlphaPercent;
mCurrentR = mOriginalR;
mCurrentG = mOriginalG;
mCurrentB = mOriginalB;
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = color;
updateAllViews();
builder.create().dismiss();
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
+ " | 透明度:" + mCurrentAlphaPercent + "%");
} finally {
isAppSelfUpdatingColor = false;
}
}
}
});
colorLayout.addView(colorBtn);
}
// 层级嵌套(滚动容器→颜色容器)
horizontalScrollView.addView(colorLayout);
builder.setView(horizontalScrollView).setNegativeButton("关闭", null).show();
}
// ====================== 点击事件实现(统一处理,逻辑清晰) ======================
@Override
public void onClick(View v) {
//ToastUtils.show("onClick");
int id = v.getId();
// 关键:所有点击事件均加判断(避免并发冲突/重复触发)
if (!isAppSelfUpdatingColor) {
if (id == R.id.iv_color_picker) {
showSystemColorPicker(); // 打开系统颜色选择器
} if (id == R.id.iv_color_scaler) {
//ToastUtils.show("iv_color_scale");
openColorScalerDialog(mCurrentColor); // 打开系统颜色选择器
} else if (id == R.id.tv_confirm) {
mListener.onColorSelected(mCurrentColor); // 确认选择,回调颜色
LogUtils.d(TAG, "confirm color | 回调颜色:" + String.format("#%08X", mCurrentColor));
dismiss();
} else if (id == R.id.tv_cancel) {
dismiss(); // 取消,关闭对话框
LogUtils.d(TAG, "cancel color | 取消选择,关闭对话框");
} else if (id == R.id.tv_brightness_minus) {
decreaseBrightness(); // 减少亮度
} else if (id == R.id.tv_brightness_plus) {
increaseBrightness(); // 增加亮度
}
}
}
void openColorScalerDialog(int nColor) {
//ToastUtils.show("openColorPickerDialog");
final ColorScalerDialog dlg = new ColorScalerDialog(getContext(), nColor);
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int color) {
dlg.currentColorScalerDialogColor = color;
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
}
class ColorScalerDialog extends ColorPickerDialog {
public int currentColorScalerDialogColor = 0;
public ColorScalerDialog(Context context, int p) {
super(context, p);
}
@Override
public void dismiss() {
super.dismiss();
int color = currentColorScalerDialogColor;
ToastUtils.show(String.format("dismiss color %d", color));
if (!isAppSelfUpdatingColor) {
isAppSelfUpdatingColor = true;
try {
mOriginalAlpha = Color.alpha(color);
mOriginalAlphaPercent = alpha2Percent(mOriginalAlpha);
mOriginalR = Color.red(color);
mOriginalG = Color.green(color);
mOriginalB = Color.blue(color);
mCurrentAlpha = mOriginalAlpha;
mCurrentAlphaPercent = mOriginalAlphaPercent;
mCurrentR = mOriginalR;
mCurrentG = mOriginalG;
mCurrentB = mOriginalB;
mCurrentBrightnessPercent = DEFAULT_BRIGHTNESS;
mCurrentColor = color;
updateAllViews();
LogUtils.d(TAG, "select system color | 选择颜色:" + String.format("#%08X", color)
+ " | 透明度:" + mCurrentAlphaPercent + "%");
} finally {
isAppSelfUpdatingColor = false;
}
}
}
}
}

View File

@@ -20,7 +20,7 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.activities.PixelPickerActivity;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;

View File

@@ -1,130 +0,0 @@
package cc.winboll.studio.powerbell.model;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类
*/
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.io.Serializable;
public class AppConfigBean extends BaseBean implements Serializable {
transient public static final String TAG = "AppConfigBean";
boolean isEnableUsegeReminder = false;
int usegeReminderValue = 45;
boolean isEnableChargeReminder = false;
int chargeReminderValue = 100;
// 铃声提醒间隔时间。.
int reminderIntervalTime = 5000;
// 电池是否正在充电。
boolean isCharging = false;
// 电池当前电量。.
int currentValue = -1;
public AppConfigBean() {
setChargeReminderValue(100);
setIsEnableChargeReminder(false);
setUsegeReminderValue(10);
setIsEnableUsegeReminder(false);
setReminderIntervalTime(5000);
}
public void setReminderIntervalTime(int reminderIntervalTime) {
this.reminderIntervalTime = reminderIntervalTime;
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
public void setIsCharging(boolean isCharging) {
this.isCharging = isCharging;
}
public boolean isCharging() {
return isCharging;
}
public void setCurrentValue(int currentValue) {
this.currentValue = currentValue;
}
public int getCurrentValue() {
return currentValue;
}
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
this.isEnableUsegeReminder = isEnableUsegeReminder;
}
public boolean isEnableUsegeReminder() {
return isEnableUsegeReminder;
}
public void setUsegeReminderValue(int usegeReminderValue) {
this.usegeReminderValue = usegeReminderValue;
}
public int getUsegeReminderValue() {
return usegeReminderValue;
}
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
this.isEnableChargeReminder = isEnableChargeReminder;
}
public boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
public void setChargeReminderValue(int chargeReminderValue) {
this.chargeReminderValue = chargeReminderValue;
}
public int getChargeReminderValue() {
return chargeReminderValue;
}
@Override
public String getName() {
return AppConfigBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
AppConfigBean bean = this;
jsonWriter.name("isEnableUsegeReminder").value(bean.isEnableUsegeReminder());
jsonWriter.name("usegeReminderValue").value(bean.getUsegeReminderValue());
jsonWriter.name("isEnableChargeReminder").value(bean.isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(bean.getChargeReminderValue());
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
AppConfigBean bean = new AppConfigBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("isEnableUsegeReminder")) {
bean.setIsEnableUsegeReminder(jsonReader.nextBoolean());
} else if (name.equals("usegeReminderValue")) {
bean.setUsegeReminderValue(jsonReader.nextInt());
} else if (name.equals("isEnableChargeReminder")) {
bean.setIsEnableChargeReminder(jsonReader.nextBoolean());
} else if (name.equals("chargeReminderValue")) {
bean.setChargeReminderValue(jsonReader.nextInt());
} else {
jsonReader.skipValue();
}
}
// 结束 JSON 对象
jsonReader.endObject();
return bean;
}
}

View File

@@ -0,0 +1,160 @@
package cc.winboll.studio.powerbell.models;
import android.util.JsonReader;
import android.util.JsonWriter;
import cc.winboll.studio.libappbase.BaseBean;
import java.io.IOException;
import java.io.Serializable;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2024/04/29 17:24:53
* @Describe 应用运行参数类(电池提醒配置、电量状态等)
*/
public class AppConfigBean extends BaseBean implements Serializable {
// ====================== 核心修复:序列化版本号(固定不变,解决崩溃) ======================
private static final long serialVersionUID = -2588138142254163296L; // 直接用崩溃日志中的流版本号
// ====================== 常量定义(默认值抽离,统一管理) ======================
public static final String TAG = "AppConfigBean";
// 默认配置常量(避免魔法值,后续修改更便捷)
private static final boolean DEFAULT_ENABLE_USAGE_REMINDER = false;
private static final int DEFAULT_USAGE_REMINDER_VALUE = 10; // 低电量提醒阈值默认10%
private static final boolean DEFAULT_ENABLE_CHARGE_REMINDER = false;
private static final int DEFAULT_CHARGE_REMINDER_VALUE = 100; // 满电提醒阈值默认100%
private static final int DEFAULT_REMINDER_INTERVAL_TIME = 5000; // 提醒间隔默认5秒
private static final boolean DEFAULT_IS_CHARGING = false; // 默认未充电
private static final int DEFAULT_CURRENT_VALUE = 100; // 默认满电
// ====================== 成员变量(规范命名+访问控制transient排除无需序列化字段 ======================
// 低电量提醒开关
private boolean isEnableUsageReminder = DEFAULT_ENABLE_USAGE_REMINDER;
// 低电量提醒阈值(百分比)
private int usageReminderValue = DEFAULT_USAGE_REMINDER_VALUE;
// 满电提醒开关
private boolean isEnableChargeReminder = DEFAULT_ENABLE_CHARGE_REMINDER;
// 满电提醒阈值(百分比)
private int chargeReminderValue = DEFAULT_CHARGE_REMINDER_VALUE;
// 铃声提醒间隔时间(毫秒)
private int reminderIntervalTime = DEFAULT_REMINDER_INTERVAL_TIME;
// 电池充电状态瞬时状态无需序列化加transient
private transient boolean isCharging = DEFAULT_IS_CHARGING;
// 当前电池电量瞬时状态无需序列化加transient
private transient int currentValue = DEFAULT_CURRENT_VALUE;
// ====================== 构造方法(简化逻辑,与默认值保持一致) ======================
public AppConfigBean() {
// 直接复用常量默认值,无需重复赋值(构造方法逻辑极简)
}
// ====================== Getter/Setter规范命名+参数校验,避免非法值) ======================
public boolean isEnableUsageReminder() {
return isEnableUsageReminder;
}
public void setEnableUsageReminder(boolean enableUsageReminder) {
isEnableUsageReminder = enableUsageReminder;
}
public int getUsageReminderValue() {
return usageReminderValue;
}
public void setUsageReminderValue(int usageReminderValue) {
// 校验电量阈值限制在0-100之间避免非法值导致业务异常
this.usageReminderValue = Math.min(Math.max(usageReminderValue, 0), 100);
}
public boolean isEnableChargeReminder() {
return isEnableChargeReminder;
}
public void setEnableChargeReminder(boolean enableChargeReminder) {
isEnableChargeReminder = enableChargeReminder;
}
public int getChargeReminderValue() {
return chargeReminderValue;
}
public void setChargeReminderValue(int chargeReminderValue) {
// 校验电量阈值限制在0-100之间
this.chargeReminderValue = Math.min(Math.max(chargeReminderValue, 0), 100);
}
public int getReminderIntervalTime() {
return reminderIntervalTime;
}
public void setReminderIntervalTime(int reminderIntervalTime) {
// 校验间隔时间不小于1秒1000ms避免过短触发频繁提醒
this.reminderIntervalTime = Math.max(reminderIntervalTime, 1000);
}
// 瞬时状态字段仅保留Get/Set无序列化影响
public boolean isCharging() {
return isCharging;
}
public void setCharging(boolean charging) {
isCharging = charging;
}
public int getCurrentValue() {
return currentValue;
}
public void setCurrentValue(int currentValue) {
// 校验当前电量限制在0-100之间
this.currentValue = Math.min(Math.max(currentValue, 0), 100);
}
// ====================== 父类方法重写补全JSON序列化逻辑兼容所有配置字段 ======================
@Override
public String getName() {
return AppConfigBean.class.getName();
}
@Override
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
super.writeThisToJsonWriter(jsonWriter);
// 仅序列化「配置类字段」瞬时状态isCharging/currentValue不序列化
jsonWriter.name("isEnableUsageReminder").value(isEnableUsageReminder());
jsonWriter.name("usageReminderValue").value(getUsageReminderValue());
jsonWriter.name("isEnableChargeReminder").value(isEnableChargeReminder());
jsonWriter.name("chargeReminderValue").value(getChargeReminderValue());
jsonWriter.name("reminderIntervalTime").value(getReminderIntervalTime()); // 补全之前遗漏的间隔时间序列化
}
@Override
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
AppConfigBean bean = new AppConfigBean();
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
switch (name) {
case "isEnableUsageReminder":
bean.setEnableUsageReminder(jsonReader.nextBoolean());
break;
case "usageReminderValue":
bean.setUsageReminderValue(jsonReader.nextInt());
break;
case "isEnableChargeReminder":
bean.setEnableChargeReminder(jsonReader.nextBoolean());
break;
case "chargeReminderValue":
bean.setChargeReminderValue(jsonReader.nextInt());
break;
case "reminderIntervalTime":
bean.setReminderIntervalTime(jsonReader.nextInt()); // 补全间隔时间反序列化
break;
default:
jsonReader.skipValue(); // 未知字段跳过,避免解析崩溃
break;
}
}
jsonReader.endObject();
return bean;
}
}

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model;
package cc.winboll.studio.powerbell.models;
import android.util.JsonReader;
import android.util.JsonWriter;

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model;
package cc.winboll.studio.powerbell.models;
/**
* @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model;
package cc.winboll.studio.powerbell.models;
import android.util.JsonReader;
import android.util.JsonWriter;

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model;
package cc.winboll.studio.powerbell.models;
/**
* @Author ZhanGSKen<zhangsken@qq.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.powerbell.model;
package cc.winboll.studio.powerbell.models;
// 应用消息结构
//

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.services.ControlCenterService;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.BatteryUtils;
@@ -50,7 +50,7 @@ public class ControlCenterServiceReceiver extends BroadcastReceiver {
appConfigUtils.loadAppConfigBean();
AppConfigBean appConfigBean = appConfigUtils.mAppConfigBean;
appConfigBean.setCurrentValue(nTheQuantityOfElectricity);
appConfigBean.setIsCharging(isCharging);
appConfigBean.setCharging(isCharging);
mwrService.get().startRemindThread(appConfigBean);
// 保存电池报告

View File

@@ -35,7 +35,7 @@ public class GlobalApplicationReceiver extends BroadcastReceiver {
// 先设置好新电池状态标志
boolean isCharging = BatteryUtils.isCharging(intent);
if (_mIsCharging != isCharging) {
mAppConfigUtils.setIsCharging(isCharging);
mAppConfigUtils.setCharging(isCharging);
}
int nTheQuantityOfElectricity = BatteryUtils.getTheQuantityOfElectricity(intent);
if (_mnTheQuantityOfElectricityOld != nTheQuantityOfElectricity) {

View File

@@ -23,15 +23,15 @@ import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.NotificationMessage;
import cc.winboll.studio.powerbell.handlers.ControlCenterServiceHandler;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.receivers.ControlCenterServiceReceiver;
import cc.winboll.studio.powerbell.services.AssistantService;
import cc.winboll.studio.powerbell.threads.RemindThread;
import cc.winboll.studio.powerbell.utils.AppCacheUtils;
import cc.winboll.studio.powerbell.utils.AppConfigUtils;
import cc.winboll.studio.powerbell.utils.NotificationHelper;
import cc.winboll.studio.powerbell.utils.NotificationManagerUtils;
import cc.winboll.studio.powerbell.utils.ServiceUtils;
import cc.winboll.studio.powerbell.utils.StringUtils;
@@ -48,7 +48,7 @@ public class ControlCenterService extends Service {
AppConfigUtils mAppConfigUtils;
AppCacheUtils mAppCacheUtils;
// 前台服务通知工具
NotificationHelper mNotificationHelper;
NotificationManagerUtils mNotificationManagerUtils;
Notification notification;
RemindThread mRemindThread;
ControlCenterServiceHandler mControlCenterServiceHandler;
@@ -72,7 +72,7 @@ public class ControlCenterService extends Service {
isServiceRunning = false;
mAppConfigUtils = App.getAppConfigUtils(this);
mAppCacheUtils = App.getAppCacheUtils(this);
mNotificationHelper = new NotificationHelper(ControlCenterService.this);
mNotificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
if (mMyServiceConnection == null) {
@@ -101,10 +101,10 @@ public class ControlCenterService extends Service {
wakeupAndBindAssistant();
// 显示前台通知栏
// 在Service中
NotificationHelper helper = new NotificationHelper(this);
Intent intent = new Intent(this, MainActivity.class);
notification = helper.showForegroundNotification(intent, getString(R.string.app_name), "Service Running, Click to open app");
startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(this);
//Intent intent = new Intent(this, MainActivity.class);
notificationManagerUtils.startForegroundServiceNotify(ControlCenterService.this, new NotificationMessage(getString(R.string.app_name), "Service Running, Click to open app"));
//startForeground(NotificationHelper.FOREGROUND_NOTIFICATION_ID, notification);
// NotificationMessage notificationMessage=createNotificationMessage();
// //Toast.makeText(getApplication(), "", Toast.LENGTH_SHORT).show();
@@ -132,9 +132,9 @@ public class ControlCenterService extends Service {
String getValuesString() {
String szReturn = "Usege: ";
szReturn += mAppConfigUtils.getIsEnableUsegeReminder() ? Integer.toString(mAppConfigUtils.getUsegeReminderValue()) : "?";
szReturn += mAppConfigUtils.isEnableUsageReminder() ? Integer.toString(mAppConfigUtils.getUsageReminderValue()) : "?";
szReturn += "% Charge: ";
szReturn += mAppConfigUtils.getIsEnableChargeReminder() ? Integer.toString(mAppConfigUtils.getChargeReminderValue()) : "?";
szReturn += mAppConfigUtils.isEnableChargeReminder() ? Integer.toString(mAppConfigUtils.getChargeReminderValue()) : "?";
szReturn += "%\nCurrent: " + Integer.toString(mAppConfigUtils.getCurrentValue()) + "%";
return szReturn;
}
@@ -185,9 +185,9 @@ public class ControlCenterService extends Service {
} else {
// 提醒进程正在进行中就更新状态后退出
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsageReminderValue());
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsageReminder());
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
mRemindThread.setIsCharging(appConfigBean.isCharging());
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
@@ -196,12 +196,12 @@ public class ControlCenterService extends Service {
}
}
mRemindThread.setChargeReminderValue(appConfigBean.getChargeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsegeReminderValue());
mRemindThread.setUsegeReminderValue(appConfigBean.getUsageReminderValue());
mRemindThread.setSleepTime(appConfigBean.getReminderIntervalTime());
mRemindThread.setIsCharging(appConfigBean.isCharging());
mRemindThread.setQuantityOfElectricity(appConfigBean.getCurrentValue());
mRemindThread.setIsEnableChargeReminder(appConfigBean.isEnableChargeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsegeReminder());
mRemindThread.setIsEnableUsegeReminder(appConfigBean.isEnableUsageReminder());
mRemindThread.start();
//LogUtils.d(TAG, "mRemindThread.start()");
}
@@ -260,9 +260,9 @@ public class ControlCenterService extends Service {
for (int i = 0; i < 20; i++) {
msg += szRemindMSG;
}
NotificationHelper helper = new NotificationHelper(ControlCenterService.this);
NotificationManagerUtils notificationManagerUtils = new NotificationManagerUtils(ControlCenterService.this);
Intent intent = new Intent(ControlCenterService.this, MainActivity.class);
helper.showTemporaryNotification(intent, getString(R.string.app_name), msg);
notificationManagerUtils.showTempAlertNotify(getString(R.string.app_name), msg);

View File

@@ -1,183 +1,249 @@
package cc.winboll.studio.powerbell.unittest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
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.MainActivity;
import cc.winboll.studio.powerbell.R;
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.views.BackgroundView;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import cc.winboll.studio.powerbell.models.BackgroundBean;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/19 18:04
* @Describe 单元测试启动主页窗口
* 终极修复版放弃FileProvider直接用私有目录File路径彻底解决UID冲突
*/
public class MainUnitTestActivity extends AppCompatActivity {
// ====================== 常量定义 ======================
public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0;
// 新增:权限请求码
public static final int REQUEST_STORAGE_PERMISSION = 1001;
View mainView;
BackgroundSourceUtils mBgSourceUtils;
BackgroundView mBackgroundView;
// 测试图片路径用Environment获取适配低版本避免硬编码
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/1764946782079.jpeg";
private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
// ====================== 成员变量移除所有Uri相关 ======================
private BackgroundView mBackgroundView;
private String mAppPrivateDirPath;
private File mPrivateTestImageFile; // 仅用File不用Uri
private File mPrivateCropImageFile;
BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法 ======================
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
setContentView(R.layout.activity_mainunittest);
mBackgroundView = findViewById(R.id.backgroundview);
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
// 裁剪测试按钮点击事件(新增权限校验)
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
ToastUtils.show("onClick准备启动裁剪");
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
// 修复1移除高版本API依赖适配低版本存储权限校验
if (checkStoragePermission()) {
// 权限已授予,启动裁剪
startCropTest();
} else {
// 权限未授予,申请权限
requestStoragePermission();
}
}
});
initBaseParams();
initViewAndEvent();
copyAssetsTestImageToPrivateDir();
//loadBackgroundByFile(); // 直接用File加载
mPreviewBackgroundBean = new BackgroundBean();
mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
mPreviewBackgroundBean.setIsUseBackgroundFile(true);
doubleRefreshPreview();
ToastUtils.show(String.format("%s onCreate", TAG));
// 加载测试图片(验证图片路径是否有效)
loadBackground();
ToastUtils.show("单元测试页面启动完成");
LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
}
/**
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
*/
private void startCropTest() {
// 修复2输出路径用Environment获取确保目录存在避免路径无效
File outputDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/");
if (!outputDir.exists()) {
outputDir.mkdirs(); // 创建目录(避免输出路径不存在导致裁剪失败)
LogUtils.d(TAG, "【裁剪测试】创建输出目录:" + outputDir.getAbsolutePath());
}
String dstOutputPath = outputDir.getAbsolutePath()
+ "/1764946782079-crop.jpeg";
// 修复3自由裁剪时比例传0避免100:100过大导致机型崩溃
ImageCropUtils.startImageCrop(
MainUnitTestActivity.this,
new File(szTestSource),
new File(dstOutputPath),
0, // 自由裁剪传0
0, // 自由裁剪传0
true,
REQUEST_CROP_IMAGE
);
}
/**
* 校验存储读写权限适配Android 6.0+ 低版本SDK移除TIRAMISU依赖
*/
private boolean checkStoragePermission() {
// 适配Android 6.0API 23及以上用通用的读写权限移除高版本API
return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
/**
* 申请存储读写权限适配低版本SDK移除READ_MEDIA_IMAGES依赖
*/
private void requestStoragePermission() {
LogUtils.d(TAG, "【裁剪测试】申请存储读写权限");
// 用通用的读写权限适配所有Android 6.0+ 机型,无高版本依赖)
ActivityCompat.requestPermissions(
this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION
);
}
/**
* 权限申请回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
// 校验权限是否授予
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "=== onActivityResult 回调 ===");
if (requestCode == REQUEST_CROP_IMAGE) {
handleCropResult(resultCode);
}
}
// ====================== 初始化相关方法 ======================
private void initBaseParams() {
LogUtils.d(TAG, "初始化基础参数:工具类+私有目录+File");
// 私有目录无需权限无UID冲突
mAppPrivateDirPath = getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBellTest/";
File privateDir = new File(mAppPrivateDirPath);
if (!privateDir.exists()) {
privateDir.mkdirs();
LogUtils.d(TAG, "创建私有目录:" + mAppPrivateDirPath);
}
// 初始化File无Uri
File refFile = new File(ASSETS_TEST_IMAGE_PATH);
String uniqueTestName = FileUtils.createUniqueFileName(refFile) + ".png";
String uniqueCropName = uniqueTestName.replace(".png", "_crop.png");
mPrivateTestImageFile = new File(mAppPrivateDirPath, uniqueTestName);
mPrivateCropImageFile = new File(mAppPrivateDirPath, uniqueCropName);
LogUtils.d(TAG, "测试图File路径" + mPrivateTestImageFile.getAbsolutePath());
}
private void initViewAndEvent() {
LogUtils.d(TAG, "初始化布局与控件事件");
setContentView(R.layout.activity_mainunittest);
mBackgroundView = (BackgroundView) findViewById(R.id.backgroundview);
// 跳转主页面按钮
Button btnMain = (Button) findViewById(R.id.btn_main_activity);
btnMain.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击按钮:跳转主页面");
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class));
}
});
// 裁剪按钮直接用File路径启动无Uri
Button btnCrop = (Button) findViewById(R.id.btn_test_cropimage);
btnCrop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "点击按钮启动裁剪File路径版");
ToastUtils.show("准备启动图片裁剪");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
startCropTestByFile(); // 直接传File
} else {
ToastUtils.show("测试图片未准备好,重新拷贝");
copyAssetsTestImageToPrivateDir();
}
}
});
}
// 从assets拷贝图片不变确保File存在
private void copyAssetsTestImageToPrivateDir() {
LogUtils.d(TAG, "开始拷贝assets图片到私有目录");
if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
LogUtils.d(TAG, "图片已存在,无需拷贝");
return;
}
InputStream inputStream = null;
try {
inputStream = getAssets().open(ASSETS_TEST_IMAGE_PATH);
FileUtils.copyStreamToFile(inputStream, mPrivateTestImageFile);
LogUtils.d(TAG, "图片拷贝成功,大小:" + mPrivateTestImageFile.length() + "字节");
} catch (IOException e) {
LogUtils.e(TAG, "图片拷贝失败:" + e.getMessage(), e);
ToastUtils.show("图片准备失败");
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.e(TAG, "关闭流失败:" + e.getMessage());
}
}
if (allGranted) {
ToastUtils.show("存储权限已授予,启动裁剪");
startCropTest(); // 权限授予后启动裁剪
} else {
ToastUtils.show("存储权限被拒绝,无法启动裁剪");
LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝");
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
LogUtils.d(TAG, "【裁剪回调】requestCode" + requestCode + "resultCode" + resultCode + "data" + (data == null ? "null" : data.toString()));
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null));
// 裁剪完成后回收权限
if (requestCode == REQUEST_CROP_IMAGE) {
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+ "/PowerBell/unittest/1764946782079-crop.jpeg";
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath));
//ImageCropUtils.releaseCropPermission(this, outputUri);
mBackgroundView.loadImage(dstOutputPath);
// ====================== 核心业务方法全改为File路径 ======================
/** 直接用File路径加载背景图无Uri无冲突 */
// private void loadBackgroundByFile() {
// LogUtils.d(TAG, "开始加载背景图File路径版");
// if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
// mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
// LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
// ToastUtils.show("背景图加载成功");
// } else {
// LogUtils.e(TAG, "背景图加载失败:文件无效");
// ToastUtils.show("背景图加载失败");
// }
// }
/** 直接用File启动裁剪关键调用ImageCropUtils的File重载方法 */
private void startCropTestByFile() {
LogUtils.d(TAG, "启动裁剪File路径版原图" + mPrivateTestImageFile.getAbsolutePath());
// 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) {
cropParent.mkdirs();
}
}
void loadBackground() {
// 校验测试图片是否存在(避免路径错误)
File testFile = new File(szTestSource);
if (testFile.exists() && testFile.length() > 100) {
mBackgroundView.loadImage(szTestSource);
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource);
// 调用ImageCropUtils的File参数方法核心绕开Uri
ImageCropUtils.startImageCrop(
this,
mPrivateTestImageFile, // 原图File
mPrivateCropImageFile, // 输出File
0,
0,
true,
REQUEST_CROP_IMAGE
);
LogUtils.d(TAG, "裁剪请求已发送,输出路径:" + mPrivateCropImageFile.getAbsolutePath());
ToastUtils.show("已启动图片裁剪");
}
/** 处理裁剪结果直接校验输出File */
private void handleCropResult(int resultCode) {
LogUtils.d(TAG, "裁剪回调处理resultCode=" + resultCode);
if (resultCode == RESULT_OK) {
if (mPrivateCropImageFile.exists() && mPrivateCropImageFile.length() > 100) {
mBackgroundView.loadImage(mPrivateCropImageFile.getAbsolutePath());
LogUtils.d(TAG, "裁剪成功,加载裁剪图:" + mPrivateCropImageFile.getAbsolutePath());
ToastUtils.show("裁剪成功");
mPreviewBackgroundBean.setIsUseBackgroundScaledCompressFile(true);
doubleRefreshPreview();
} else {
LogUtils.e(TAG, "裁剪成功但输出文件无效");
ToastUtils.show("裁剪失败:输出文件无效");
}
} else if (resultCode == RESULT_CANCELED) {
LogUtils.d(TAG, "裁剪取消");
ToastUtils.show("裁剪已取消");
} else {
ToastUtils.show("测试图片不存在或无效");
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource);
LogUtils.e(TAG, "裁剪失败resultCode异常");
ToastUtils.show("裁剪失败");
}
}
}
/**
* 双重刷新预览,确保背景加载最新数据
* 移除:缓存清空逻辑
*/
private void doubleRefreshPreview() {
// 第一重刷新
try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第一重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第一重异常:" + e.getMessage());
return;
}
// 第二重刷新(延迟执行)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (mBackgroundView != null && !isFinishing()) {
try {
mBackgroundView.loadBackgroundBean(mPreviewBackgroundBean, true);
mBackgroundView.setBackgroundColor(mPreviewBackgroundBean.getPixelColor());
LogUtils.d(TAG, "【双重刷新】第二重完成");
} catch (Exception e) {
LogUtils.e(TAG, "【双重刷新】第二重异常:" + e.getMessage());
}
}
}
}, 200);
}
}

View File

@@ -2,7 +2,7 @@ package cc.winboll.studio.powerbell.utils;
import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList;
public class AppCacheUtils {

View File

@@ -5,8 +5,8 @@ import android.content.Context;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.model.AppConfigBean;
import cc.winboll.studio.powerbell.model.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.models.AppConfigBean;
import cc.winboll.studio.powerbell.models.ControlCenterServiceBean;
import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.fragments.MainViewFragment;
import cc.winboll.studio.powerbell.services.ControlCenterService;
@@ -93,22 +93,22 @@ public class AppConfigUtils {
return bean.isEnableService();
}
public void setIsEnableChargeReminder(boolean isEnableChargeReminder) {
mAppConfigBean.setIsEnableChargeReminder(isEnableChargeReminder);
public void setEnableChargeReminder(boolean isEnableChargeReminder) {
mAppConfigBean.setEnableChargeReminder(isEnableChargeReminder);
saveConfigData(MainActivity._mMainActivity);
}
public boolean getIsEnableChargeReminder() {
public boolean isEnableChargeReminder() {
return mAppConfigBean.isEnableChargeReminder();
}
public void setIsEnableUsegeReminder(boolean isEnableUsegeReminder) {
mAppConfigBean.setIsEnableUsegeReminder(isEnableUsegeReminder);
public void setEnableUsageReminder(boolean isEnableUsegeReminder) {
mAppConfigBean.setEnableUsageReminder(isEnableUsegeReminder);
saveConfigData(MainActivity._mMainActivity);
}
public boolean getIsEnableUsegeReminder() {
return mAppConfigBean.isEnableUsegeReminder();
public boolean isEnableUsageReminder() {
return mAppConfigBean.isEnableUsageReminder();
}
public void setChargeReminderValue(int value) {
@@ -120,17 +120,17 @@ public class AppConfigUtils {
return mAppConfigBean.getChargeReminderValue();
}
public void setUsegeReminderValue(int value) {
mAppConfigBean.setUsegeReminderValue(value);
public void setUsageReminderValue(int value) {
mAppConfigBean.setUsageReminderValue(value);
saveConfigData(MainActivity._mMainActivity);
}
public int getUsegeReminderValue() {
return mAppConfigBean.getUsegeReminderValue();
public int getUsageReminderValue() {
return mAppConfigBean.getUsageReminderValue();
}
public void setIsCharging(boolean isCharging) {
mAppConfigBean.setIsCharging(isCharging);
public void setCharging(boolean isCharging) {
mAppConfigBean.setCharging(isCharging);
}
public boolean isCharging() {
@@ -162,9 +162,9 @@ public class AppConfigUtils {
bean = new AppConfigBean();
AppConfigBean.saveBean(mContext, mAppConfigBean);
}
mAppConfigBean.setIsEnableUsegeReminder(bean.isEnableUsegeReminder());
mAppConfigBean.setUsegeReminderValue(bean.getUsegeReminderValue());
mAppConfigBean.setIsEnableChargeReminder(bean.isEnableChargeReminder());
mAppConfigBean.setEnableUsageReminder(bean.isEnableUsageReminder());
mAppConfigBean.setUsageReminderValue(bean.getUsageReminderValue());
mAppConfigBean.setEnableChargeReminder(bean.isEnableChargeReminder());
mAppConfigBean.setChargeReminderValue(bean.getChargeReminderValue());
}

View File

@@ -11,7 +11,7 @@ import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.BuildConfig;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -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);
@@ -212,6 +212,10 @@ public class BackgroundSourceUtils {
return false;
}
String genNewCropFileName() {
return UUID.randomUUID().toString() + System.currentTimeMillis();
}
/**
* 创建并更新预览剪裁环境
*/
@@ -226,11 +230,13 @@ public class BackgroundSourceUtils {
return true;
}
Uri uri = UriUtil.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
String fileSuffix = FileUtils.getFileSuffix(mContext, uri);
String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis();
Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
LogUtils.d(TAG, String.format("createAndUpdatePreviewEnvironmentForCropping: uri %s", uri));
String fileSuffix = UriUtils.getSuffixFromUri(mContext, uri);
LogUtils.d(TAG, String.format("createAndUpdatePreviewEnvironmentForCropping: fileSuffix = %s", fileSuffix));
String newCropFileName = genNewCropFileName();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix);
mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + ".png");
if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) {
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile);
@@ -415,6 +421,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

@@ -263,24 +263,28 @@ public class FileUtils {
}
}
public static String getFileSuffix(Context context, Uri uri){
String szType = context.getContentResolver().getType(uri);
// 2. 截取MIME类型后缀如从image/jpeg中提取jpeg【核心新增逻辑】
String fileSuffix = "";
if (szType != null && szType.contains("/")) {
// 分割字符串,取"/"后面的部分(如"image/jpeg" → 分割后取索引1的"jpeg"
fileSuffix = szType.split("/")[1];
// 调试日志:打印截取后的文件后缀
} else {
// 异常处理若类型为空或格式错误默认后缀设为jpeg保留原逻辑兼容性
fileSuffix = "jpeg";
}
return fileSuffix;
}
public static boolean isFileExists(String path) {
File file = new File(path);
return file.exists();
}
/**
* 获取文件后缀(不带点,忽略大小写,适配空文件名/无后缀场景)
* @param file 目标文件
* @return 后缀字符串(无后缀返回空字符串,非空统一小写)
*/
public static String getFileSuffix(File file) {
if (file == null || file.getName().isEmpty()) {
return ""; // 空文件/空文件名,返回空
}
String fileName = file.getName();
int lastDotIndex = fileName.lastIndexOf(".");
// 无后缀(没有点,或点在开头/结尾)
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == fileName.length() - 1) {
return "";
}
// 截取后缀并转小写(统一格式,避免 PNG/png 差异)
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
}

View File

@@ -6,42 +6,101 @@ import android.net.Uri;
import android.os.Build;
import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import com.yalantis.ucrop.UCropActivity;
import java.io.File;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import com.yalantis.ucrop.UCrop;
import java.io.File;
/**
* 图片裁剪工具类集成uCrop,脱离系统依赖
* 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File 双传参
*/
public class ImageCropUtils {
public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与项目一致)
// FileProvider 授权(与 AndroidManifest 配置一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 强制输出格式:固定为 PNG保留透明通道
private static final String FORCE_OUTPUT_SUFFIX = "png";
private static final android.graphics.Bitmap.CompressFormat FORCE_COMPRESS_FORMAT = android.graphics.Bitmap.CompressFormat.PNG;
// ====================== 核心裁剪方法(强制 PNG 输出,优化逻辑)======================
/**
* 【Uri 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param inputUri 输入图片 Uri本应用 FileProvider Uri非空
* @param outputUri 输出图片 Uri本应用 FileProvider Uri非空
* @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
public static void startImageCrop(Activity activity,
Uri inputUri,
Uri outputUri,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return;
}
if (inputUri == null || outputUri == null) {
LogUtils.e(TAG, "【裁剪异常】输入/输出 Uri 为空");
showToast(activity, "图片 Uri 无效,无法裁剪");
return;
}
if (!isValidUri(activity, inputUri)) {
LogUtils.e(TAG, "【裁剪异常】输入 Uri 无效:" + inputUri);
showToast(activity, "原图 Uri 无效,无法裁剪");
return;
}
// 2. 核心:强制修正输出为 PNG忽略原图格式统一转 PNG
File outputFile = uriToFile(activity, outputUri);
if (outputFile == null) {
LogUtils.e(TAG, "【裁剪异常】输出 Uri 转 File 失败:" + outputUri);
showToast(activity, "裁剪输出路径无效");
return;
}
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
outputUri = getFileProviderUri(activity, outputFile); // 重新生成 PNG 对应的 Uri
// 3. 初始化 uCrop + 强制 PNG 配置(保留透明核心)
UCrop uCrop = UCrop.of(inputUri, outputUri);
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 4. 启动裁剪
uCrop.withOptions(options);
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【裁剪启动成功Uri 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
}
/**
* 启动uCrop裁剪(核心方法,替代系统裁剪)
* 【File 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
* @param activity 上下文
* @param inputFile 输入图片文件
* @param outputFile 输出图片文件
* @param isFreeCrop 是否自由裁剪true=自由false=固定比例
* @param inputFile 输入图片文件(任意格式)
* @param outputFile 输出图片文件(最终强制转为 PNG
* @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码
*/
public static void startImageCrop(Activity activity,
File inputFile,
File outputFile,
int aspectX,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
// 校验输入参数
// 1. 输入参数校验
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】上下文Activity无效");
LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return;
}
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
LogUtils.e(TAG, "【裁剪异常】输入文件无效");
LogUtils.e(TAG, "【裁剪异常】输入图片文件无效");
showToast(activity, "无有效图片可裁剪");
return;
}
@@ -51,47 +110,28 @@ public class ImageCropUtils {
return;
}
// 生成输入/输出Uri适配FileProvider
// 2. 核心:强制修正输出为 PNG忽略原图格式
Uri inputUri = getFileProviderUri(activity, inputFile);
Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri兼容低版本
outputFile = correctFileSuffix(outputFile, FORCE_OUTPUT_SUFFIX); // 强制 .png 后缀
Uri outputUri = getFileProviderUri(activity, outputFile);
// 配置uCrop参数
// 3. 初始化 uCrop + 强制 PNG 配置
UCrop uCrop = UCrop.of(inputUri, outputUri);
UCrop.Options options = new UCrop.Options();
uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 裁剪模式配置(自由裁剪/固定比例)
if (isFreeCrop) {
// 自由裁剪:无固定比例,可随意调整
uCrop.withAspectRatio(0, 0);
options.setFreeStyleCropEnabled(true); // 开启自由裁剪
} else {
// 固定比例默认1:1可根据需求修改
uCrop.withAspectRatio(aspectX, aspectY);
options.setFreeStyleCropEnabled(false);
}
// 裁剪配置(优化体验)
options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
options.setCompressionQuality(100); // 图片质量
options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
options.setToolbarTitle("图片裁剪"); // 工具栏标题
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
// 应用配置并启动裁剪
// 4. 启动裁剪
uCrop.withOptions(options);
// 启动uCrop裁剪Activity替代系统裁剪
uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【uCrop启动】成功输入Uri" + inputUri + "输出Uri" + outputUri + ",请求码:" + requestCode);
LogUtils.d(TAG, "【裁剪启动成功File 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
}
/**
* 重载方法:适配BackgroundBean
* BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
*/
public static void startImageCrop(Activity activity,
BackgroundBean cropBean,
int aspectX,
int aspectX,
int aspectY,
boolean isFreeCrop,
int requestCode) {
@@ -100,70 +140,163 @@ public class ImageCropUtils {
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
}
/**
* 生成FileProvider Uri
*/
private static Uri getFileProviderUri(Activity activity, File file) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
Uri uri = FileProvider.getUriForFile(activity, authority, file);
LogUtils.d(TAG, "Uri生成】FileProvider Uri" + uri);
return uri;
} else {
Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "【Uri生成】普通Uri" + uri);
return uri;
// ====================== 裁剪结果处理(保持兼容,优化日志)======================
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
if (requestCode != cropRequestCode) return null;
if (resultCode == Activity.RESULT_OK && data != null) {
Uri outputUri = UCrop.getOutput(data);
if (outputUri != null) {
String outputPath = uriToPath(outputUri);
LogUtils.d(TAG, "裁剪成功】强制输出 PNG透明保留输出路径" + outputPath);
return outputPath;
}
} else if (resultCode == UCrop.RESULT_ERROR) {
Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "【裁剪失败】原因:" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【裁剪取消】用户手动取消");
}
return null;
}
// ====================== 辅助方法(优化适配强制 PNG 逻辑)======================
/** 校验 Uri 有效性(确保是图片类型) */
private static boolean isValidUri(Activity activity, Uri uri) {
try {
String type = activity.getContentResolver().getType(uri);
return type != null && type.startsWith("image/");
} catch (Exception e) {
LogUtils.e(TAG, "【Uri生成】失败" + e.getMessage());
LogUtils.e(TAG, "【Uri 校验失败】原因" + e.getMessage());
return false;
}
}
/** Uri 转 File适配 FileProvider Uri 和普通 Uri */
private static File uriToFile(Activity activity, Uri uri) {
if (uri == null) return null;
try {
if (uri.getScheme().equals("file")) {
return new File(uri.getPath());
}
String filePath = uri.getPath();
if (filePath == null) return null;
if (filePath.contains("/external_files/")) {
filePath = filePath.replace("/external_files/", activity.getExternalFilesDir("").getAbsolutePath() + "/");
} else if (filePath.contains("/cache/")) {
filePath = filePath.replace("/cache/", activity.getCacheDir().getAbsolutePath() + "/");
}
return new File(filePath);
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 转 File 失败】uri=" + uri + ",原因:" + e.getMessage());
return null;
}
}
/** Uri 提取文件路径 */
private static String uriToPath(Uri uri) {
if (uri == null) return null;
try {
if (uri.getScheme().equals("file")) {
return uri.getPath();
}
String path = uri.getPath();
if (path == null) return null;
String[] prefixes = {"/external/", "/external_files/", "/cache/", "/files/"};
for (String prefix : prefixes) {
if (path.contains(prefix)) {
path = path.substring(path.indexOf(prefix) + prefix.length());
String externalRoot = android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
return externalRoot + "/" + path;
}
}
return path;
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 转路径失败】uri=" + uri + ",原因:" + e.getMessage());
return null;
}
}
/**
* 处理uCrop裁剪回调在Activity的onActivityResult中调用
* @param requestCode 请求码
* @param resultCode 结果码
* @param data 回调数据
* @return 裁剪成功返回输出文件路径失败返回null
* 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心
* 移除 isPng 参数,全程用 PNG 配置
*/
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
// 校验是否是uCrop的回调
if (requestCode == cropRequestCode) {
if (resultCode == Activity.RESULT_OK && data != null) {
// 裁剪成功获取输出Uri
Uri outputUri = UCrop.getOutput(data);
if (outputUri != null) {
String outputPath = outputUri.getPath();
LogUtils.d(TAG, "【uCrop回调】裁剪成功输出路径" + outputPath);
return outputPath;
}
} else if (resultCode == UCrop.RESULT_ERROR) {
// 裁剪失败,获取异常信息
Throwable error = UCrop.getError(data);
LogUtils.e(TAG, "【uCrop回调】裁剪失败" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【uCrop回调】裁剪被取消");
}
}
return null;
private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
UCrop.Options options = new UCrop.Options();
// 裁剪模式配置(自由裁剪/固定比例)
options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
// 裁剪配置(优化体验)
//options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
//options.setCompressionQuality(100); // 图片质量
//options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
//options.setToolbarTitle("图片裁剪"); // 工具栏标题
//options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
//options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
// 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
options.setDimmedLayerColor(activity.getResources().getColor(android.R.color.transparent)); // 遮罩透明(关键)
options.setCropFrameColor(activity.getResources().getColor(R.color.colorPrimary)); // 裁剪框主题色
options.setCropGridColor(activity.getResources().getColor(R.color.colorAccent)); // 网格线主题色
// 3. 通用 UI 配置(保持原有风格)
options.setHideBottomControls(true); // 隐藏底部控制栏
options.setToolbarTitle("图片裁剪");
options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
options.setToolbarWidgetColor(activity.getResources().getColor(android.R.color.white));
options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark));
return options;
}
/**
* 显示Toast
* 修正文件后缀(强制转为 .png覆盖原有任何图片后缀
*/
private static File correctFileSuffix(File originFile, String targetSuffix) {
String originName = originFile.getName();
// 强制替换所有图片后缀为 targetSuffix避免漏改
originName = originName.replaceAll("\\.(jpg|jpeg|png|bmp|gif)$", "") + "." + targetSuffix;
return new File(originFile.getParent(), originName);
}
/** 生成 FileProvider Uri适配 Android 7.0+ */
private static Uri getFileProviderUri(Activity activity, File file) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX;
return FileProvider.getUriForFile(activity, authority, file);
} else {
return Uri.fromFile(file);
}
} catch (Exception e) {
LogUtils.e(TAG, "【Uri 生成失败】原因:" + e.getMessage());
return null;
}
}
/** 显示 Toast避免崩溃 */
private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
}
}
/**
* 暴露getFileProviderUri方法供外部调用
*/
// ====================== 公有辅助方法(供外部调用)======================
public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, file);
}
public static File getFileFromUriPublic(Activity activity, Uri uri) {
return uriToFile(activity, uri);
}
public static String getPathFromUriPublic(Uri uri) {
return uriToPath(uri);
}
}

View File

@@ -1,151 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2025/03/22 04:39:40
* @Describe 通知工具类
*/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.powerbell.R;
public class NotificationHelper {
public static final String TAG = "NotificationHelper";
// 渠道ID和名称
private static final String CHANNEL_ID_FOREGROUND = "foreground_channel";
private static final String CHANNEL_NAME_FOREGROUND = "Foreground Service";
private static final String CHANNEL_ID_TEMPORARY = "temporary_channel";
private static final String CHANNEL_NAME_TEMPORARY = "Temporary Notifications";
// 通知ID
public static final int FOREGROUND_NOTIFICATION_ID = 1001;
public static final int TEMPORARY_NOTIFICATION_ID = 2001;
private final Context mContext;
private final NotificationManager mNotificationManager;
public NotificationHelper(Context context) {
mContext = context;
mNotificationManager = context.getSystemService(NotificationManager.class);
createNotificationChannels();
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundChannel();
createTemporaryChannel();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createForegroundChannel() {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_FOREGROUND,
CHANNEL_NAME_FOREGROUND,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Persistent service notifications");
channel.setSound(null, null);
channel.enableVibration(false);
mNotificationManager.createNotificationChannel(channel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createTemporaryChannel() {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_TEMPORARY,
CHANNEL_NAME_TEMPORARY,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Temporary alert notifications");
channel.setSound(null, null);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
channel.setBypassDnd(true);
mNotificationManager.createNotificationChannel(channel);
}
// 显示常驻通知(通常用于前台服务)
public Notification showForegroundNotification(Intent intent, String title, String content) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_FOREGROUND)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
//.setContentTitle(title + "\n" + content)
.setContentTitle(content)
//.setContentText(content)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build();
mNotificationManager.notify(FOREGROUND_NOTIFICATION_ID, notification);
return notification;
}
// 显示临时通知(自动消失)
public void showTemporaryNotification(Intent intent, String title, String content) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
.setContentTitle(title)
.setContentText(content)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setVibrate(new long[]{100, 200, 300, 400})
.build();
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID, notification);
}
// 创建自定义布局通知(可扩展)
public void showCustomNotification(Intent intent, RemoteViews contentView, RemoteViews bigContentView) {
PendingIntent pendingIntent = createPendingIntent(intent);
Notification notification = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMPORARY)
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(pendingIntent)
.setContent(contentView)
.setCustomBigContentView(bigContentView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build();
mNotificationManager.notify(TEMPORARY_NOTIFICATION_ID + 1, notification);
}
// 取消所有通知
public void cancelAllNotifications() {
mNotificationManager.cancelAll();
}
// 创建PendingIntent兼容不同API版本
private PendingIntent createPendingIntent(Intent intent) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// flags |= PendingIntent.FLAG_IMMUTABLE;
// }
return PendingIntent.getActivity(
mContext,
0,
intent,
flags
);
}
}

View File

@@ -0,0 +1,416 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.os.Build;
import android.view.View;
import android.widget.RemoteViews;
import androidx.core.app.NotificationCompat;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/13 20:44
* @Describe 全局通知管理工具类整合所有通知能力适配API29-30兼容Java7所有通知统一跳转MainActivity
*/
public class NotificationManagerUtils {
// ====================== 常量定义(统一管理,避免冲突,首屏可见)======================
public static final String TAG = "NotificationManagerUtils";
// 通知渠道4大渠道场景隔离API26+必填)
// 1. 前台服务保活渠道(低优先级,无打扰)
private static final String CHANNEL_ID_FOREGROUND_SERVICE = "channel_foreground_service";
private static final String CHANNEL_NAME_FOREGROUND_SERVICE = "前台服务保活通知";
private static final String CHANNEL_DESC_FOREGROUND_SERVICE = "后台服务运行状态,无声音无震动,不打扰用户";
// 2. 电量提醒渠道(高优先级,闹钟铃声+震动,强提醒)
private static final String CHANNEL_ID_BATTERY_REMIND = "channel_battery_remind";
private static final String CHANNEL_NAME_BATTERY_REMIND = "电量异常提醒通知";
private static final String CHANNEL_DESC_BATTERY_REMIND = "电量过高/过低提醒,强震动+闹钟铃声,突破免打扰";
// 3. 通用临时通知渠道(高优先级,仅震动,普通告警)
private static final String CHANNEL_ID_TEMP_ALERT = "channel_temp_alert";
private static final String CHANNEL_NAME_TEMP_ALERT = "通用临时提醒通知";
private static final String CHANNEL_DESC_TEMP_ALERT = "普通即时告警,仅震动提醒,自动取消";
// 通知ID唯一区分避免覆盖按场景分段
public static final int NOTIFY_ID_FOREGROUND_SERVICE = 1001; // 前台服务
public static final int NOTIFY_ID_BATTERY_REMIND = 1002; // 电量提醒
public static final int NOTIFY_ID_TEMP_ALERT = 1003; // 通用临时通知
public static final int NOTIFY_ID_CUSTOM_LAYOUT = 1004; // 自定义布局通知
// 通用配置
private static final int PENDING_INTENT_FLAGS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? 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 final Context mContext;
private final NotificationManager mNotificationManager;
// 前台服务通知专属
private Notification mForegroundServiceNotify;
private RemoteViews mForegroundServiceRemoteViews;
// 电量提醒通知专属
private Notification mBatteryRemindNotify;
private RemoteViews mBatteryRemindRemoteViews;
// ====================== 构造方法(单例思想/实例化通用,自动初始化渠道)======================
public NotificationManagerUtils(Context context) {
LogUtils.d(TAG, "【初始化】全局通知管理工具类 构造方法调用");
this.mContext = context.getApplicationContext(); // 用应用上下文,避免内存泄漏
this.mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
createAllNotificationChannels(); // 自动创建所有渠道API26+
LogUtils.d(TAG, "【初始化】全局通知管理工具类 完成,渠道创建状态:" + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "已创建4个渠道" : "无需创建"));
}
// ====================== 核心基础能力(渠道创建+Intent构建复用逻辑减少冗余======================
/**
* 创建所有通知渠道API26+专属,低版本自动跳过,确保通知正常显示)
*/
@SuppressWarnings("deprecation")
public void createAllNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LogUtils.d(TAG, "【渠道管理】开始创建所有通知渠道");
createForegroundServiceChannel();
createBatteryRemindChannel();
createTempAlertChannel();
LogUtils.d(TAG, "【渠道管理】4个通知渠道创建完成含3个核心渠道+预留扩展)");
}
}
/**
* 创建前台服务保活渠道IMPORTANCE_LOW无声音无震动不打扰用户
*/
private void createForegroundServiceChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_FOREGROUND_SERVICE,
CHANNEL_NAME_FOREGROUND_SERVICE,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription(CHANNEL_DESC_FOREGROUND_SERVICE);
channel.setSound(null, null);
channel.enableVibration(false);
channel.setShowBadge(false); // 不显示应用角标
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); // 锁屏隐藏
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】前台服务保活渠道创建成功:" + CHANNEL_NAME_FOREGROUND_SERVICE);
}
}
/**
* 创建电量提醒渠道IMPORTANCE_HIGH闹钟铃声+震动,突破免打扰)
*/
private void createBatteryRemindChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_BATTERY_REMIND,
CHANNEL_NAME_BATTERY_REMIND,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_BATTERY_REMIND);
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(true); // 突破免打扰
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); // 锁屏可见
channel.setShowBadge(true);
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】电量提醒渠道创建成功:" + CHANNEL_NAME_BATTERY_REMIND);
}
}
/**
* 创建通用临时通知渠道IMPORTANCE_HIGH仅震动普通告警
*/
private void createTempAlertChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID_TEMP_ALERT,
CHANNEL_NAME_TEMP_ALERT,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(CHANNEL_DESC_TEMP_ALERT);
//channel.setSound(null, null); // 仅震动,不发声
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), null); // 闹钟铃声
channel.enableVibration(true);
channel.setVibrationPattern(VIBRATE_PATTERN);
channel.setBypassDnd(false); // 不突破免打扰
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
channel.setShowBadge(true);
mNotificationManager.createNotificationChannel(channel);
LogUtils.d(TAG, "【渠道管理】通用临时通知渠道创建成功:" + CHANNEL_NAME_TEMP_ALERT);
}
}
/**
* 构建固定跳转PendingIntent所有通知统一跳转MainActivity适配API29-30安全规范
* @return 安全的PendingIntent确保跳转稳定不泄露
*/
private PendingIntent buildFixedPendingIntent() {
// 固定跳MainActivity不支持自定义目标
Intent intent = new Intent(mContext, MainActivity.class);
// API29+ 强制要求:明确包名,避免跳转目标模糊
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,
0,
intent,
PENDING_INTENT_FLAGS
);
LogUtils.d(TAG, "【Intent构建】PendingIntent创建成功安全标志" + PENDING_INTENT_FLAGS);
return pendingIntent;
}
// ====================== 场景1前台服务保活通知支持自定义布局+更新)======================
/**
* 初始化前台服务通知自定义布局RemoteViews
*/
private void initForegroundServiceRemoteViews(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【布局初始化】开始初始化前台服务通知布局,标题:" + msg.getTitle());
mForegroundServiceRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView1, msg.getTitle());
mForegroundServiceRemoteViews.setTextViewText(R.id.remoteviewTextView3, msg.getContent());
mForegroundServiceRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
LogUtils.d(TAG, "【布局初始化】前台服务通知布局填充完成");
}
/**
* 启动前台服务保活通知ControlCenterService专用API26+强制要求,保活后台服务)
*/
public void startForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【前台服务通知】开始构建保活通知,内容:" + msg.getContent());
if (service == null || msg == null) {
LogUtils.e(TAG, "【前台服务通知】构建失败Service/NotificationMessage为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知兼容API26+渠道低版本用Builder
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(service, CHANNEL_ID_FOREGROUND_SERVICE);
} else {
builder = new Notification.Builder(service);
}
mForegroundServiceNotify = builder
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentTitle(msg.getTitle())
.setContentText(msg.getContent())
.setWhen(System.currentTimeMillis())
.setColor(Color.parseColor("#F00606")) // 小图标背景色
.setContentIntent(pendingIntent)
.setOngoing(true) // 常驻通知,不可滑动取消(保活关键)
.setAutoCancel(false) // 禁止点击取消
.build();
// 3. 设置自定义布局
initForegroundServiceRemoteViews(service, msg);
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
// 4. 启动前台服务必须调用否则Service易被回收
service.startForeground(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "【前台服务通知】保活通知启动成功通知ID" + NOTIFY_ID_FOREGROUND_SERVICE);
}
/**
* 更新前台服务保活通知内容(无需重启服务,直接刷新布局)
*/
public void updateForegroundServiceNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【前台服务通知】开始更新保活通知,新内容:" + msg.getContent());
if (mForegroundServiceNotify == null || mForegroundServiceRemoteViews == null) {
LogUtils.e(TAG, "【前台服务通知】更新失败通知对象未初始化先调用startForegroundServiceNotify");
return;
}
// 更新自定义布局数据
initForegroundServiceRemoteViews(service, msg);
mForegroundServiceNotify.contentView = mForegroundServiceRemoteViews;
mForegroundServiceNotify.bigContentView = mForegroundServiceRemoteViews;
// 发送更新
mNotificationManager.notify(NOTIFY_ID_FOREGROUND_SERVICE, mForegroundServiceNotify);
LogUtils.d(TAG, "【前台服务通知】保活通知更新成功");
}
// ====================== 场景2电量提醒通知支持自定义布局+更新+单独取消)======================
/**
* 初始化电量提醒通知自定义布局RemoteViews支持充电/耗电切换)
*/
private void initBatteryRemindRemoteViews(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【布局初始化】开始初始化电量提醒布局,提醒类型:" + msg.getRemindMSG());
mBatteryRemindRemoteViews = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
mBatteryRemindRemoteViews.setTextViewText(R.id.viewremindnotificationTextView1, msg.getTitle());
mBatteryRemindRemoteViews.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
// 切换布局(+:充电提醒,-:耗电提醒)
String remindType = msg.getRemindMSG() != null ? msg.getRemindMSG().trim() : "";
if ("+".equals(remindType)) {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:充电提醒");
} else if ("-".equals(remindType)) {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
LogUtils.d(TAG, "【布局初始化】电量提醒布局切换:耗电提醒");
} else {
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mBatteryRemindRemoteViews.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
LogUtils.w(TAG, "【布局初始化】未知电量提醒类型:" + remindType);
}
}
/**
* 初始化电量提醒通知仅构建不发送配合update触发提醒
*/
public void initBatteryRemindNotify(ControlCenterService service, NotificationMessage msg) {
LogUtils.d(TAG, "【电量提醒通知】开始初始化提醒通知,标题:" + msg.getTitle());
if (service == null || msg == null) {
LogUtils.e(TAG, "【电量提醒通知】初始化失败Service/NotificationMessage为空");
return;
}
// 1. 构建固定跳转Intent统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建基础通知
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(service, CHANNEL_ID_BATTERY_REMIND);
} else {
builder = new Notification.Builder(service);
}
mBatteryRemindNotify = builder
.setSmallIcon(R.drawable.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentTitle(msg.getTitle())
.setContentText(msg.getContent())
.setWhen(System.currentTimeMillis())
.setColor(Color.parseColor("#F00606"))
.setContentIntent(pendingIntent)
.setAutoCancel(true) // 点击取消
.build();
// 3. 初始化自定义布局
initBatteryRemindRemoteViews(service, msg);
mBatteryRemindNotify.contentView = mBatteryRemindRemoteViews;
mBatteryRemindNotify.bigContentView = mBatteryRemindRemoteViews;
LogUtils.d(TAG, "【电量提醒通知】初始化完成");
}
/**
* 发送/更新电量提醒通知(初始化后调用,触发强提醒)
*/
public void sendOrUpdateBatteryRemindNotify() {
LogUtils.d(TAG, "【电量提醒通知】开始发送/更新提醒");
if (mBatteryRemindNotify == null || mBatteryRemindRemoteViews == null) {
LogUtils.e(TAG, "【电量提醒通知】发送失败通知未初始化先调用initBatteryRemindNotify");
return;
}
mNotificationManager.notify(NOTIFY_ID_BATTERY_REMIND, mBatteryRemindNotify);
LogUtils.d(TAG, "【电量提醒通知】发送/更新成功通知ID" + NOTIFY_ID_BATTERY_REMIND);
}
/**
* 单独取消电量提醒通知(静态方法,外部可直接调用,无需实例化)
*/
public static void cancelBatteryRemindNotify(Context context) {
LogUtils.d(TAG, "【电量提醒通知】开始取消提醒通知ID" + NOTIFY_ID_BATTERY_REMIND);
if (context == null) {
LogUtils.e(TAG, "【电量提醒通知】取消失败Context为空");
return;
}
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFY_ID_BATTERY_REMIND);
LogUtils.d(TAG, "【电量提醒通知】取消成功");
}
// ====================== 场景3通用临时通知简单文本自动取消无需自定义布局======================
/**
* 显示通用临时通知普通告警仅震动自动取消统一跳转MainActivity
* @param title 通知标题
* @param content 通知内容
*/
public void showTempAlertNotify(String title, String content) {
LogUtils.d(TAG, "【通用临时通知】开始构建,标题:" + title + ",内容:" + content);
if (title == null || content == null) {
LogUtils.e(TAG, "【通用临时通知】构建失败:标题/内容为空");
return;
}
// 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)
.setLargeIcon(BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher))
.setContentTitle(title)
.setContentText(content)
.setWhen(System.currentTimeMillis())
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setVibrate(VIBRATE_PATTERN);
// 3. 发送通知
Notification notification = builder.build();
mNotificationManager.notify(NOTIFY_ID_TEMP_ALERT, notification);
LogUtils.d(TAG, "【通用临时通知】显示成功通知ID" + NOTIFY_ID_TEMP_ALERT);
}
// ====================== 场景4自定义布局通知灵活扩展支持复杂样式======================
/**
* 显示自定义布局通知(支持普通布局+大布局通用所有场景统一跳转MainActivity
* @param contentView 普通自定义布局(必填)
* @param 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统一跳MainActivity
PendingIntent pendingIntent = buildFixedPendingIntent();
// 2. 构建自定义布局通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, CHANNEL_ID_TEMP_ALERT)
.setSmallIcon(R.drawable.ic_launcher) // 必传,不可省略
.setContentIntent(pendingIntent)
.setContent(contentView)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
// 添加大布局(可选)
if (bigContentView != null) {
builder.setCustomBigContentView(bigContentView);
LogUtils.d(TAG, "【自定义布局通知】已添加下拉大布局布局ID" + bigContentView.getLayoutId());
}
// 3. 发送通知
Notification notification = builder.build();
mNotificationManager.notify(NOTIFY_ID_CUSTOM_LAYOUT, notification);
LogUtils.d(TAG, "【自定义布局通知】显示成功通知ID" + NOTIFY_ID_CUSTOM_LAYOUT);
}
// ====================== 通知取消工具(支持精准取消/全取消)======================
/**
* 取消指定ID的通知精准取消灵活控制
*/
public void cancelNotifyById(int notifyId) {
LogUtils.d(TAG, "【通知管理】开始取消通知ID" + notifyId);
mNotificationManager.cancel(notifyId);
LogUtils.d(TAG, "【通知管理】通知取消成功ID" + notifyId);
}
/**
* 取消所有通知(谨慎使用,会清除所有场景的通知)
*/
public void cancelAllNotifies() {
LogUtils.d(TAG, "【通知管理】开始取消所有通知");
mNotificationManager.cancelAll();
LogUtils.d(TAG, "【通知管理】所有通知取消完成");
}
}

View File

@@ -1,247 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/*
* 参考:
* https://blog.csdn.net/qq_35507234/article/details/90676587
* https://blog.csdn.net/qq_16628781/article/details/51548324
*/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.os.Build;
import android.view.View;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.model.NotificationMessage;
import cc.winboll.studio.powerbell.services.ControlCenterService;
public class NotificationUtils2 {
public static final String TAG = NotificationHelper.class.getSimpleName();
Context mContext;
NotificationManager mNotificationManager;
Notification mForegroundNotification;
PendingIntent mForegroundPendingIntent;
Notification mRemindNotification;
PendingIntent mRemindPendingIntent;
RemoteViews mrvServiceNotificationView;
RemoteViews mrvRemindNotificationView;
static enum NotificationType { MIN, MAX };
private static int _mnServiceNotificationID = 1;
private static int _mnRemindNotificationID = 2;
private static String _mszChannelIDService = "1";
private static String _mszChannelNameService = "Service";
private static String _mszChannelIDRemind = "2";
private static String _mszChannelNameRemind = "Remind";
// public NotificationUtils(Context context) {
// mContext = context;
// mNotificationManager = (NotificationManager) context.getSystemService(
// Context.NOTIFICATION_SERVICE);
// }
public NotificationUtils2(Context context) {
mContext = context;
mNotificationManager = context.getSystemService(NotificationManager.class);
//createNotificationChannels();
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createServiceChannel();
createRemindChannel();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createServiceChannel() {
NotificationChannel channel = new NotificationChannel(
_mszChannelIDService,
_mszChannelNameService,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Background service updates");
channel.setSound(null, null);
channel.enableVibration(false);
mNotificationManager.createNotificationChannel(channel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void createRemindChannel() {
NotificationChannel channel = new NotificationChannel(
_mszChannelIDRemind,
_mszChannelNameRemind,
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Critical reminders");
channel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM), null);
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400});
channel.setBypassDnd(true);
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
mNotificationManager.createNotificationChannel(channel);
}
// 创建并发送服务通知
//
public void createForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//创建Notification传入Context和channelId
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
intent.setPackage(service.getPackageName());
//LogUtils.d(TAG, "mService.getPackageName() : " + service.getPackageName());
intent.setClass(service, MainActivity.class);
//LogUtils.d(TAG, "MainActivity.class.getName() : " + MainActivity.class.getName());
//这里放一个count用来区分每一个通知
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
//参数1:context 上下文对象
//参数2:发送者私有的请求码(Private request code for the sender)
//参数3:intent 意图对象
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
//mForegroundPendingIntent = PendingIntent.getActivity(mService, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mForegroundPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mForegroundPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_IMMUTABLE);
}
mForegroundNotification = new Notification.Builder(service, _mszChannelIDService)
.setAutoCancel(true)
.setContentTitle(notificationMessage.getTitle())
.setContentText(notificationMessage.getContent())
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_launcher)
//设置红色
.setColor(Color.parseColor("#F00606"))
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentIntent(mForegroundPendingIntent)
.build();
setForegroundNotificationRemoteViews(service, notificationMessage);
service.startForeground(_mnServiceNotificationID, mForegroundNotification);
}
void initmrvRemindNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
mrvRemindNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_remindnotification);
mrvRemindNotificationView.setTextViewText(R.id.viewremindnotificationTextView1, notificationMessage.getTitle());
String szRemindMSG = notificationMessage.getRemindMSG();
//LogUtils.d(TAG, "szRemindMSG : " + szRemindMSG);
//mrvRemindNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
if (szRemindMSG != null) {
if (szRemindMSG.trim().equals("-")) {
//LogUtils.d(TAG, "-");
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.GONE);
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.VISIBLE);
} else if (szRemindMSG.trim().equals("+")) {
//LogUtils.d(TAG, "+");
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewUsege, View.GONE);
mrvRemindNotificationView.setViewVisibility(R.id.remoteviewCharge, View.VISIBLE);
}
mrvRemindNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
//给我remoteViews上的控件tv_content添加监听事件
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
//return mrvServiceNotificationView;
}
}
void initmrvServiceNotificationView(ControlCenterService service, NotificationMessage notificationMessage) {
mrvServiceNotificationView = new RemoteViews(service.getPackageName(), R.layout.view_servicenotification);
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView1, notificationMessage.getTitle());
//String szRemindMSG = notificationMessage.getRemindMSG();
//mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView2, szRemindMSG);
//rvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent() + Integer.toString(nTest));
mrvServiceNotificationView.setTextViewText(R.id.remoteviewTextView3, notificationMessage.getContent());
mrvServiceNotificationView.setImageViewResource(R.id.remoteviewImageView1, R.drawable.ic_launcher);
//给我remoteViews上的控件tv_content添加监听事件
//remoteViews.setOnClickPendingIntent(R.id.remoteviewLinearLayout1, pi);
//return mrvServiceNotificationView;
}
void setForegroundNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
initmrvServiceNotificationView(service, notificationMessage);
mForegroundNotification.contentView = mrvServiceNotificationView;
mForegroundNotification.bigContentView = mrvServiceNotificationView;
}
void setRemindNotificationRemoteViews(ControlCenterService service, NotificationMessage notificationMessage) {
initmrvRemindNotificationView(service, notificationMessage);
mRemindNotification.contentView = mrvRemindNotificationView;
mRemindNotification.bigContentView = mrvRemindNotificationView;
}
// 更新服务通知
//
public void updateForegroundNotification(ControlCenterService service, NotificationMessage notificationMessage) {
setForegroundNotificationRemoteViews(service, notificationMessage);
mNotificationManager.notify(_mnServiceNotificationID, mForegroundNotification);
}
// 创建并发送电量提醒通知
//
public void updateRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//LogUtils.d(TAG, "updateRemindNotification : " + notificationMessage.getRemindMSG());
setRemindNotificationRemoteViews(service, notificationMessage);
mNotificationManager.notify(_mnRemindNotificationID, mRemindNotification);
}
public void createRemindNotification(ControlCenterService service, NotificationMessage notificationMessage) {
//LogUtils.d(TAG, "notificationMessage : " + notificationMessage.getRemindMSG());
//创建Notification传入Context和channelId
Intent intent = new Intent();//这个intent会传给目标,可以使用getIntent来获取
intent.setPackage(service.getPackageName());
intent.setClass(service, MainActivity.class);
//这里放一个count用来区分每一个通知
//intent.putExtra("intent", "intent--->" + count);//这里设置一个数据,带过去
//参数1:context 上下文对象
//参数2:发送者私有的请求码(Private request code for the sender)
//参数3:intent 意图对象
//参数4:必须为FLAG_ONE_SHOT,FLAG_NO_CREATE,FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT,中的一个
//mRemindPendingIntent = PendingIntent.getActivity(mService, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mRemindPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mRemindPendingIntent = PendingIntent.getActivity(service,
1, intent, PendingIntent.FLAG_IMMUTABLE);
}
mRemindNotification = new Notification.Builder(service, _mszChannelIDRemind)
.setAutoCancel(true)
.setContentTitle(notificationMessage.getTitle())
.setContentText(notificationMessage.getContent())
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.ic_launcher)
//设置红色
.setColor(Color.parseColor("#F00606"))
.setLargeIcon(BitmapFactory.decodeResource(service.getResources(), R.drawable.ic_launcher))
.setContentIntent(mRemindPendingIntent)
.build();
setRemindNotificationRemoteViews(service, notificationMessage);
}
public static void cancelRemindNotification(Context context){
// 获取 NotificationManager 实例
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 撤回指定 ID 的通知栏消息
notificationManager.cancel(_mnRemindNotificationID);
}
}

View File

@@ -1,32 +1,47 @@
package cc.winboll.studio.powerbell.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R;
/**
* 权限申请工具类
* 适配 Android 13+ 媒体权限 & 低版本存储权限
* 兼容 SDK 版本低于 33 的编译环境
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/14 03:05
* @Describe 权限申请工具类Java7兼容版
* 适配 小米手机+API29-30整合自启动、电池优化、全文件管理权限专注后台保活核心权限
*/
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; // 自启动权限(小米专属)
public static final int REQUEST_ALL_FILE_MANAGE = 1002; // 全文件管理权限API30+
// SDK版本常量适配API29-30替代系统枚举Java7兼容
private static final int SDK_VERSION_Q = 29; // Android 10API29
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 +51,300 @@ public class PermissionUtils {
synchronized (PermissionUtils.class) {
if (sInstance == null) {
sInstance = new PermissionUtils();
LogUtils.d(TAG, "初始化PermissionUtils 单例创建成功");
}
}
}
return sInstance;
}
// ====================== 核心权限1全文件管理权限API29-30适配通用所有机型======================
/**
* 检查并请求 存储权限Android 12及以下
* 检查全文件管理权限适配API30+ MANAGE_EXTERNAL_STORAGE兼容API29-旧权限
* @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
public boolean checkAllFileManagePermission(Activity activity) {
LogUtils.d(TAG, "全文件权限-检查:开始校验,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null) {
LogUtils.e(TAG, "全文件权限-检查失败Activity为空");
return false;
}
// API30+:校验 MANAGE_EXTERNAL_STORAGE 特殊权限
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
boolean hasManagePerm = Environment.isExternalStorageManager();
LogUtils.d(TAG, "全文件权限-检查API30+MANAGE_EXTERNAL_STORAGE权限=" + (hasManagePerm ? "已授予" : "未授予"));
return hasManagePerm;
} else if (Build.VERSION.SDK_INT == SDK_VERSION_Q) {
LogUtils.d(TAG, "全文件权限-检查API29无需申请默认支持文件管理");
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;
}
/**
* 检查并请求 媒体图片权限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;
}
/**
* 权限请求结果处理
*/
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);
} else {
boolean hasWritePerm = ContextCompat.checkSelfPermission(activity,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
LogUtils.d(TAG, "全文件权限-检查API29以下WRITE_EXTERNAL_STORAGE权限=" + (hasWritePerm ? "已授予" : "未授予"));
return hasWritePerm;
}
}
/**
* 检查是否有管理所有文件权限Android 11+
* 申请全文件管理权限适配API30+特殊权限流程兼容API29-旧权限申请
* @param activity 申请权限的Activity不可为null
*/
public boolean checkManageExternalStoragePermission(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return android.os.Environment.isExternalStorageManager();
public void requestAllFileManagePermission(Activity activity) {
LogUtils.d(TAG, "全文件权限-申请:开始处理,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "全文件权限-申请失败Activity无效/已销毁");
return;
}
return true;
}
/**
* 请求管理所有文件权限Android 11+
*/
public void requestManageExternalStoragePermission(Activity activity, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 先检查权限,已授予直接返回
if (checkAllFileManagePermission(activity)) {
LogUtils.d(TAG, "全文件权限-申请:已拥有权限,无需发起");
return;
}
// 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);
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
LogUtils.d(TAG, "全文件权限-申请API30+,跳转特殊权限申请页");
} catch (Exception e) {
LogUtils.e(TAG, "请求管理文件权限异常:" + e.getMessage());
// 备用跳转:系统设置首页,引导手动操作
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_ALL_FILE_MANAGE);
LogUtils.w(TAG, "全文件权限-申请:跳转失败,引导手动开启");
showAllFileManageTipsDialog(activity);
}
} else {
ActivityCompat.requestPermissions(activity,
new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_ALL_FILE_MANAGE);
LogUtils.d(TAG, "全文件权限-申请API29以下发起WRITE_EXTERNAL_STORAGE权限申请");
}
}
// ====================== 核心权限2自启动权限小米专属API29-30适配======================
/**
* 检查自启动权限(仅小米机型需要,非小米直接返回无需申请)
* @param activity 上下文Activity不可为null
* @return true=小米机型需手动开启false=非小米机型(无需申请)
*/
// public boolean checkAutoStartPermission(Activity activity) {
// LogUtils.d(TAG, "自启动权限-检查:开始,设备品牌=" + Build.BRAND);
// if (activity == null) {
// LogUtils.e(TAG, "自启动权限-检查失败Activity为空");
// return false;
// }
//
// boolean isXiaomi = Build.BRAND.toLowerCase().contains("xiaomi");
// LogUtils.d(TAG, "自启动权限-检查:结果=" + (isXiaomi ? "小米机型(需开启)" : "非小米机型(无需申请)"));
// return isXiaomi;
// }
/**
* 请求自启动权限小米专属多方案跳转适配API29-30机型差异
* @param activity 申请权限的Activity不可为null
*/
public void requestAutoStartPermission(Activity activity) {
LogUtils.d(TAG, "自启动权限-申请:开始处理");
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "自启动权限-申请失败Activity无效/已销毁");
return;
}
// 非小米机型,直接返回
// if (!checkAutoStartPermission(activity)) {
// LogUtils.d(TAG, "自启动权限-申请:非小米机型,无需处理");
// return;
// }
// API30+ 小米:优先精准跳转自启动管理页
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
try {
// 方案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, "自启动权限-申请API30+,组件名跳转自启动管理页");
} 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, "自启动权限-申请API30+Action跳转自启动管理页");
} catch (Exception e2) {
// 方案3终极备用跳转系统设置+提示
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.w(TAG, "自启动权限-申请:跳转失败,引导手动操作");
showAutoStartTipsDialog(activity);
}
}
return;
}
// API29 小米:低版本兼容跳转
try {
Intent intent = new Intent(XIAOMI_AUTO_START_CLASS);
intent.setPackage(XIAOMI_AUTO_START_PACKAGE);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-申请API29低版本跳转自启动管理页");
} catch (Exception e) {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START);
showAutoStartTipsDialog(activity);
}
}
// ====================== 核心权限3电池优化权限通用所有机型API29-30适配======================
/**
* 检查忽略电池优化权限精准判断API23+有效,低版本视为已拥有)
* @param activity 上下文Activity不可为null
* @return true=已忽略优化false=未忽略(需申请)
*/
public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) {
LogUtils.d(TAG, "电池优化权限-检查:开始,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null) {
LogUtils.e(TAG, "电池优化权限-检查失败Activity为空");
return false;
}
// 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;
}
/**
* 请求忽略电池优化权限多方案跳转适配API29-30自动判断是否需要申请
* @param activity 申请权限的Activity不可为null
*/
public void requestIgnoreBatteryOptimizationPermission(Activity activity) {
LogUtils.d(TAG, "电池优化权限-申请:开始处理");
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "电池优化权限-申请失败Activity无效/已销毁");
return;
}
// 已拥有权限,直接返回
if (checkIgnoreBatteryOptimizationPermission(activity)) {
LogUtils.d(TAG, "电池优化权限-申请:已拥有权限,无需发起");
return;
}
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 showAllFileManageTipsDialog(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 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, "电池优化权限:显示手动开启提示弹窗");
}
public void startPermissionRequest(final Activity activity) {
// 电池优化权限(通用所有机型)
if (!checkIgnoreBatteryOptimizationPermission(activity)) {
YesNoAlertDialog.show(activity, activity.getString(R.string.app_name) + "权限申请提示:", "本应用要正常使用,需要申请电池优化与自启动权限。是否进入权限设置步骤?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onNo() {
ToastUtils.show(activity.getString(R.string.app_name) + "应用可能无法正常使用。");
}
@Override
public void onYes() {
requestIgnoreBatteryOptimizationPermission(activity);
}
});
}
}
public void handlePermissionRequest(final Activity activity, int requestCode, int resultCode, Intent data) {
if (requestCode == PermissionUtils.REQUEST_IGNORE_BATTERY_OPTIMIZATION) {
// 自启动权限(小米专属)
// 小米机型,发起自启动权限申请
requestAutoStartPermission(activity);
} else if (requestCode == PermissionUtils.REQUEST_AUTO_START) {
// 自启动权限(小米专属)
if (App.isDebugging() && !checkAllFileManagePermission(activity)) {
// 小米机型,发起自启动权限申请
requestAllFileManagePermission(activity);
}
}
}
}

View File

@@ -1,6 +1,6 @@
package cc.winboll.studio.powerbell.utils;
import cc.winboll.studio.powerbell.model.BatteryInfoBean;
import cc.winboll.studio.powerbell.models.BatteryInfoBean;
import java.util.ArrayList;
public class StringUtils {

View File

@@ -1,217 +0,0 @@
package cc.winboll.studio.powerbell.utils;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/28 04:23:04
* @Describe UriUtil
*/
import android.content.ContentResolver;
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;
import java.io.InputStream;
import java.io.OutputStream;
public class UriUtil {
public static final String TAG = "UriUtil";
/**
* 获取真实路径
*
* @param context
*/
public static String getFilePathFromUri(Context context, Uri uri) {
if (uri == null) {
return null;
}
switch (uri.getScheme()) {
case ContentResolver.SCHEME_CONTENT:
//Android7.0之后的uri content:// URI
return getFilePathFromContentUri(context, uri);
case ContentResolver.SCHEME_FILE:
default:
//Android7.0之前的uri file://
return new File(uri.getPath()).getAbsolutePath();
}
}
/**
* 从uri获取path
*
* @param uri content://media/external/file/109009
* <p>
* FileProvider适配
* content://com.tencent.mobileqq.fileprovider/external_files/storage/emulated/0/Tencent/QQfile_recv/
* content://com.tencent.mm.external.fileprovider/external/tencent/MicroMsg/Download/
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
if (null == uri) return null;
String data = null;
String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
int index = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
} else {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
data = getPathFromInputStreamUri(context, uri, fileName);
}
}
cursor.close();
}
return data;
}
/**
* 用流拷贝文件一份到自己APP私有目录下
*
* @param context
* @param uri
* @param fileName
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
InputStream inputStream = null;
String filePath = null;
if (uri.getAuthority() != null) {
try {
inputStream = context.getContentResolver().openInputStream(uri);
File file = createTemporalFileFrom(context, inputStream, fileName);
filePath = file.getPath();
} catch (Exception e) {
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
}
}
}
return 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);
// 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);
}
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;
if (inputStream != null) {
int read;
byte[] buffer = new byte[8 * 1024];
//自己定义拷贝文件路径
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
outputStream.flush();
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return targetFile;
}
}

View File

@@ -0,0 +1,481 @@
package cc.winboll.studio.powerbell.utils;
import android.content.ContentResolver;
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;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* Uri 工具类Java7兼容适配API29-30+小米机型FileProvider安全适配
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2024/06/28
*/
public class UriUtils {
// ====================== 常量定义(顶部统一管理)======================
public static final String TAG = "UriUtils";
// FileProvider 授权后缀与Manifest配置保持一致
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider";
// 应用公共图片目录API29+ 适配替代废弃API
private static final String APP_PUBLIC_PIC_DIR = "PowerBell/";
// MIME类型与文件后缀映射表覆盖常见格式小米机型精准匹配
private static final Map<String, String> MIME_SUFFIX_MAP = new HashMap<String, String>() {{
// 图片格式(重点,含透明格式)
put("image/png", "png");
put("image/jpeg", "jpg");
put("image/jpg", "jpg");
put("image/gif", "gif");
put("image/bmp", "bmp");
put("image/webp", "webp");
// 音视频格式
put("video/mp4", "mp4");
put("video/avi", "avi");
put("video/mkv", "mkv");
put("audio/mp3", "mp3");
put("audio/wav", "wav");
// 文档格式
put("application/pdf", "pdf");
put("application/msword", "doc");
put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
put("application/vnd.ms-excel", "xls");
put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
}};
// ====================== 新增核心方法Uri 转文件后缀 ======================
/**
* 【静态公共方法】根据 Uri 获取文件真实后缀优先MIME类型匹配适配所有Uri场景+小米机型)
* @param context 上下文非空用于获取ContentResolver
* @param uri 待解析 Uri支持 content:// / file:// 双Scheme
* @return 小写文件后缀(如 png/jpg/mp4无匹配返回空字符串
*/
public static String getSuffixFromUri(Context context, Uri uri) {
LogUtils.d(TAG, "=== getSuffixFromUri 调用 startUri" + (uri != null ? uri.toString() : "null") + " ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getSuffixFromUriContext 为空,获取失败");
return "";
}
if (uri == null) {
LogUtils.e(TAG, "getSuffixFromUriUri 为空,获取失败");
return "";
}
String suffix = "";
String scheme = uri.getScheme();
// 2. 按 Uri Scheme 分类处理(优先精准匹配,再降级截取)
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
// 场景1content:// Uri优先通过MIME类型获取最精准
suffix = getSuffixFromContentUri(context, uri);
LogUtils.d(TAG, "getSuffixFromUricontent:// UriMIME匹配后缀" + suffix);
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
// 场景2file:// Uri直接解析文件名截取后缀
String filePath = new File(uri.getPath()).getAbsolutePath();
suffix = getSuffixFromFilePath(filePath);
LogUtils.d(TAG, "getSuffixFromUrifile:// Uri路径截取后缀" + suffix);
} else {
// 场景3未知Scheme尝试解析Uri路径截取兜底
String uriPath = uri.getPath();
suffix = uriPath != null ? getSuffixFromFilePath(uriPath) : "";
LogUtils.w(TAG, "getSuffixFromUri未知Scheme=" + scheme + ",兜底截取后缀:" + suffix);
}
// 3. 最终结果处理(统一小写,去空)
suffix = suffix != null ? suffix.trim().toLowerCase() : "";
LogUtils.d(TAG, "=== getSuffixFromUri 调用 end最终后缀" + suffix + " ===");
return suffix;
}
// ====================== 公有核心方法(对外提供能力,按功能排序)======================
/**
* Uri 转真实文件路径(核心方法,适配 content:// / file:// 双 Scheme
* @param context 上下文(非空)
* @param uri 待转换 Uri非空
* @return 真实文件绝对路径(转换失败返回 null
*/
public static String getFilePathFromUri(Context context, Uri uri) {
LogUtils.d(TAG, "=== getFilePathFromUri 调用 start ===");
if (context == null) {
LogUtils.e(TAG, "getFilePathFromUriContext 为空,转换失败");
return null;
}
if (uri == null) {
LogUtils.e(TAG, "getFilePathFromUriUri 为空,转换失败");
return null;
}
String scheme = uri.getScheme();
String filePath = null;
// 按 Uri Scheme 分类处理
if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
LogUtils.d(TAG, "getFilePathFromUriScheme=content执行ContentUri转换");
filePath = getFilePathFromContentUri(context, uri);
} else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
LogUtils.d(TAG, "getFilePathFromUriScheme=file直接转换路径");
filePath = new File(uri.getPath()).getAbsolutePath();
} else {
LogUtils.w(TAG, "getFilePathFromUri未知Scheme=" + scheme + ",转换失败");
}
LogUtils.d(TAG, "=== getFilePathFromUri 调用 end结果" + filePath + " ===");
return filePath;
}
/**
* 文件路径转 Uri核心方法适配 Android7.0+ FileProviderAPI29-30兼容
* @param context 上下文(非空)
* @param filePath 真实文件路径(非空)
* @return 安全 Uri转换失败返回 null
*/
public static Uri getUriForFile(Context context, String filePath) {
LogUtils.d(TAG, "=== getUriForFile路径版调用 start ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getUriForFileContext 为空,转换失败");
return null;
}
if (filePath == null || filePath.isEmpty()) {
LogUtils.e(TAG, "getUriForFile文件路径为空转换失败");
return null;
}
// 2. File 对象初始化与校验
File file = new File(filePath);
LogUtils.d(TAG, "getUriForFile文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 3. 合法路径校验适配小米机型避免FileProvider配置外路径
if (!isPathInValidDir(context, file)) {
LogUtils.w(TAG, "getUriForFile路径不在安全配置目录内小米机型可能出现权限异常");
}
// 4. 调用重载方法生成 Uri
Uri uri = getUriForFile(context, file);
LogUtils.d(TAG, "=== getUriForFile路径版调用 end结果" + (uri != null ? uri.toString() : "null") + " ===");
return uri;
}
/**
* File 对象转 Uri重载方法直接接收File内部安全适配
* @param context 上下文(非空)
* @param file 待转换 File 对象(非空)
* @return 安全 Uri转换失败返回 null
*/
public static Uri getUriForFile(Context context, File file) {
LogUtils.d(TAG, "=== getUriForFileFile版调用 start ===");
// 1. 基础参数校验
if (context == null) {
LogUtils.e(TAG, "getUriForFileContext 为空,转换失败");
return null;
}
if (file == null) {
LogUtils.e(TAG, "getUriForFileFile 对象为空,转换失败");
return null;
}
LogUtils.d(TAG, "getUriForFile文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
if (!file.exists() || file.isDirectory()) {
LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 2. 按系统版本生成 UriAPI24+ 强制 FileProvider适配小米机型
Uri uri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LogUtils.d(TAG, "getUriForFileAndroid7.0+使用FileProvider生成Uri");
String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX;
LogUtils.d(TAG, "getUriForFileFileProvider Authority=" + authority);
try {
uri = FileProvider.getUriForFile(context, authority, file);
LogUtils.d(TAG, "getUriForFileContent Uri生成成功=" + uri.toString());
} catch (IllegalArgumentException e) {
LogUtils.e(TAG, "getUriForFileFileProvider生成失败小米机型常见原因路径未配置/Authority不匹配", e);
}
} else {
LogUtils.d(TAG, "getUriForFileAndroid7.0以下使用Uri.fromFile生成");
uri = Uri.fromFile(file);
LogUtils.d(TAG, "getUriForFileFile Uri生成成功=" + uri.toString());
}
LogUtils.d(TAG, "=== getUriForFileFile版调用 end ===");
return uri;
}
// ====================== 私有辅助方法(内部逻辑封装,不对外暴露)======================
/**
* ContentUri 转真实路径(适配 content:// 格式处理小米机型特殊Uri
* @param context 上下文
* @param uri ContentUricontent://media/external/file/xxx
* @return 真实文件路径(失败返回 null
*/
private static String getFilePathFromContentUri(Context context, Uri uri) {
LogUtils.d(TAG, "getFilePathFromContentUriUri=" + uri.toString());
String filePath = null;
Cursor cursor = null;
// Java7 语法try-catch-finally 手动关闭Cursor避免内存泄漏
try {
// 查询字段:优先 DATA 字段,失败则通过文件名+流拷贝获取
String[] queryColumns = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
// 优先读取 DATA 字段(直接获取路径)
int dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
if (dataIndex != -1) {
filePath = cursor.getString(dataIndex);
LogUtils.d(TAG, "getFilePathFromContentUri从DATA字段获取路径=" + filePath);
} else {
// DATA 字段为空,通过流拷贝到私有目录获取路径(小米机型特殊场景适配)
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
String fileName = cursor.getString(nameIndex);
LogUtils.d(TAG, "getFilePathFromContentUriDATA字段为空通过流拷贝获取文件名=" + fileName);
filePath = getPathFromInputStreamUri(context, uri, fileName);
}
}
} catch (Exception e) {
LogUtils.e(TAG, "getFilePathFromContentUri查询失败", e);
} finally {
// 强制关闭Cursor避免资源泄漏Java7 必须手动处理)
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
LogUtils.e(TAG, "getFilePathFromContentUri关闭Cursor失败", e);
}
}
}
return filePath;
}
/**
* 流拷贝获取路径(适配无 DATA 字段的 ContentUri小米机型特殊Uri兼容
* 将目标文件拷贝到应用私有缓存目录,返回拷贝后的路径
* @param context 上下文
* @param uri ContentUri
* @param fileName 文件名
* @return 拷贝后的文件路径(失败返回 null
*/
private static String getPathFromInputStreamUri(Context context, Uri uri, String fileName) {
LogUtils.d(TAG, "getPathFromInputStreamUri开始流拷贝文件名=" + fileName);
InputStream inputStream = null;
OutputStream outputStream = null;
File targetFile = null;
try {
// 1. 打开输入流读取Uri对应文件
inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) {
LogUtils.e(TAG, "getPathFromInputStreamUri输入流打开失败");
return null;
}
// 2. 创建目标文件(应用私有缓存目录,无权限限制)
targetFile = new File(context.getExternalCacheDir(), fileName);
// 若文件已存在,先删除(避免覆盖导致格式异常)
if (targetFile.exists()) {
boolean deleteSuccess = targetFile.delete();
LogUtils.d(TAG, "getPathFromInputStreamUri删除已存在文件结果=" + deleteSuccess);
}
// 3. 流拷贝Java7 手动处理流,避免 try-with-resources
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[8 * 1024]; // 8KB 缓冲区,平衡效率与内存
int readLength;
while ((readLength = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readLength);
}
outputStream.flush();
LogUtils.d(TAG, "getPathFromInputStreamUri流拷贝成功路径=" + targetFile.getAbsolutePath());
} catch (Exception e) {
LogUtils.e(TAG, "getPathFromInputStreamUri流拷贝失败", e);
// 拷贝失败,删除临时文件
if (targetFile != null && targetFile.exists()) {
targetFile.delete();
}
targetFile = null;
} finally {
// 强制关闭流避免资源泄漏Java7 必须手动关闭)
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
LogUtils.e(TAG, "getPathFromInputStreamUri关闭输出流失败", e);
}
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
LogUtils.e(TAG, "getPathFromInputStreamUri关闭输入流失败", e);
}
}
return targetFile != null ? targetFile.getAbsolutePath() : null;
}
/**
* 校验路径是否在安全目录内适配API29-30+小米机型避免FileProvider权限异常
* 仅允许:应用私有目录、缓存目录、应用专属公共目录
* @param context 上下文
* @param file 待校验文件
* @return true=安全路径false=非安全路径
*/
private static boolean isPathInValidDir(Context context, File file) {
String absolutePath = file.getAbsolutePath();
// 1. 应用外部私有目录API29+ 推荐,无权限限制)
String externalPrivateDir = context.getExternalFilesDir(null) != null
? context.getExternalFilesDir(null).getAbsolutePath()
: "";
// 2. 应用内部私有目录(无权限限制)
String internalPrivateDir = context.getFilesDir().getAbsolutePath();
// 3. 应用缓存目录(无权限限制)
String cacheDir = context.getCacheDir().getAbsolutePath();
// 4. 应用专属公共目录API29+ 适配,替代废弃的 getExternalStoragePublicDirectory
String appPublicDir = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + Environment.DIRECTORY_PICTURES
+ File.separator + APP_PUBLIC_PIC_DIR;
// 校验路径是否在安全目录内小米机型必须严格校验否则FileProvider会抛异常
boolean isInValidDir = absolutePath.startsWith(externalPrivateDir)
|| absolutePath.startsWith(internalPrivateDir)
|| absolutePath.startsWith(cacheDir)
|| absolutePath.startsWith(appPublicDir);
LogUtils.d(TAG, "isPathInValidDir外部私有目录=" + externalPrivateDir
+ ",公共目录=" + appPublicDir
+ ",校验结果=" + isInValidDir);
return isInValidDir;
}
/**
* 流拷贝创建临时文件(内部辅助,封装拷贝逻辑)
* @param context 上下文
* @param inputStream 输入流
* @param fileName 文件名
* @return 临时文件(失败返回 null
* @throws IOException 流操作异常
*/
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) throws IOException {
File targetFile = null;
if (inputStream != null) {
byte[] buffer = new byte[8 * 1024];
int readLength;
targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) {
targetFile.delete();
}
OutputStream outputStream = new FileOutputStream(targetFile);
while ((readLength = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readLength);
}
outputStream.flush();
outputStream.close();
}
return targetFile;
}
/**
* 辅助ContentUri 通过 MIME 类型获取后缀(精准匹配,不受文件名伪造影响)
* @param context 上下文
* @param uri ContentUri
* @return 匹配的后缀(无匹配返回空字符串)
*/
private static String getSuffixFromContentUri(Context context, Uri uri) {
String mime = null;
try {
// 通过 ContentResolver 获取 Uri 对应的 MIME 类型(系统级匹配,最精准)
mime = context.getContentResolver().getType(uri);
LogUtils.d(TAG, "getSuffixFromContentUri获取MIME类型=" + mime);
if (mime == null || mime.isEmpty()) {
// MIME 为空,尝试解析文件名兜底
String fileName = getFileNameFromContentUri(context, uri);
return getSuffixFromFilePath(fileName);
}
// MIME 类型匹配后缀(优先完全匹配,再模糊匹配)
if (MIME_SUFFIX_MAP.containsKey(mime)) {
return MIME_SUFFIX_MAP.get(mime);
}
// 模糊匹配(如 image/* 匹配通用图片后缀默认png
if (mime.startsWith("image/")) {
return "png";
} else if (mime.startsWith("video/")) {
return "mp4";
} else if (mime.startsWith("audio/")) {
return "mp3";
} else if (mime.startsWith("application/")) {
return "pdf";
}
} catch (Exception e) {
LogUtils.e(TAG, "getSuffixFromContentUriMIME解析失败mime=" + mime, e);
}
// 所有方式失败解析Uri路径兜底
return getSuffixFromFilePath(uri.getPath());
}
/**
* 辅助:从 ContentUri 获取文件名MIME 解析失败时兜底)
* @param context 上下文
* @param uri ContentUri
* @return 文件名(失败返回空字符串)
*/
private static String getFileNameFromContentUri(Context context, Uri uri) {
Cursor cursor = null;
try {
String[] queryColumns = {MediaStore.MediaColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
return cursor.getString(nameIndex);
}
} catch (Exception e) {
LogUtils.e(TAG, "getFileNameFromContentUri查询失败", e);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
LogUtils.e(TAG, "getFileNameFromContentUri关闭Cursor失败", e);
}
}
}
return "";
}
/**
* 辅助:从文件路径/文件名截取后缀(兜底方案,处理各种路径格式)
* @param path 文件路径/文件名
* @return 截取的后缀(无后缀返回空字符串)
*/
private static String getSuffixFromFilePath(String path) {
if (path == null || path.isEmpty()) {
return "";
}
// 处理路径中的分隔符(兼容 Windows/Android 路径格式)
path = path.replace("\\", "/");
// 取最后一个 "/" 后的文件名(避免路径包含 "." 导致误判)
int lastSepIndex = path.lastIndexOf("/");
if (lastSepIndex != -1 && lastSepIndex < path.length() - 1) {
path = path.substring(lastSepIndex + 1);
}
// 截取最后一个 "." 后的后缀(过滤无后缀/点开头/点结尾场景)
int lastDotIndex = path.lastIndexOf(".");
if (lastDotIndex == -1 || lastDotIndex == 0 || lastDotIndex == path.length() - 1) {
return "";
}
// 过滤后缀中的非法字符(仅保留字母/数字,避免特殊字符干扰)
String suffix = path.substring(lastDotIndex + 1).replaceAll("[^a-zA-Z0-9]", "");
// 限制后缀长度1-5位避免超长伪造后缀
return suffix.length() >= 1 && suffix.length() <= 5 ? suffix : "";
}
}

View File

@@ -14,7 +14,7 @@ import android.widget.RelativeLayout;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.model.BackgroundBean;
import cc.winboll.studio.powerbell.models.BackgroundBean;
import java.io.File;
/**

View File

@@ -14,7 +14,7 @@ public class BatteryDrawable extends Drawable {
// 电量颜色画笔
final Paint mPaint;
// 电量值
int mnValue = 1;
int mnValue = 100;
// @int color 电量颜色
//

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/holo_blue_light" />
<corners android:radius="6dp" />
</shape>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:浅灰色背景 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#E0E0E0" /> <!-- 按压深色 -->
<corners android:radius="8dp" /> <!-- 圆角适配小米UI风格 -->
<stroke android:width="1dp" android:color="#CCCCCC" /> <!-- 边框 -->
</shape>
</item>
<!-- 正常状态:白色背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" /> <!-- 正常白色 -->
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#CCCCCC" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:深灰色背景 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#D0D0D0" />
<corners android:radius="8dp" /> <!-- 与亮度按钮圆角一致,统一风格 -->
</shape>
</item>
<!-- 正常状态:浅灰色背景 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="8dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:深灰色 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#CCCCCC" /> <!-- 按压时颜色 -->
<corners android:radius="8dp" /> <!-- 圆角(可按需调整) -->
<stroke android:width="1dp" android:color="#EEEEEE" /> <!-- 边框(可选,不加可删除) -->
</shape>
</item>
<!-- 正常状态:浅灰色 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#F5F5F5" /> <!-- 正常时颜色 -->
<corners android:radius="8dp" /> <!-- 圆角(和按压状态一致) -->
<stroke android:width="1dp" android:color="#EEEEEE" /> <!-- 边框(可选) -->
</shape>
</item>
</selector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按压状态:加深深色背景 -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#2D7CFF" /> <!-- 按压深蓝 -->
<corners android:radius="8dp" />
</shape>
</item>
<!-- 正常状态:主色背景(可改为项目主题色) -->
<item>
<shape android:shape="rectangle">
<solid android:color="#4096FF" /> <!-- 正常浅蓝适配小米系统UI -->
<corners android:radius="8dp" />
</shape>
</item>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 背景色:白色 -->
<solid android:color="@android:color/white" />
<!-- 圆角12dp适配小米机型圆角无锯齿 -->
<corners android:radius="12dp" />
<!-- 边框:浅灰色细边框(避免弹窗边缘模糊) -->
<stroke
android:width="1dp"
android:color="@android:color/darker_gray" />
<!-- 内边距:轻微留白,避免内容贴边 -->
<padding
android:bottom="5dp"
android:top="5dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="6dp" />
<stroke
android:width="1dp"
android:color="@android:color/darker_gray" />
</shape>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 进度条未完成部分:浅灰色 -->
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<corners android:radius="10dp" />
</shape>
</item>
<!-- 进度条已完成部分:系统蓝色(无需额外定义颜色) -->
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle">
<solid android:color="@android:color/holo_blue_light" />
<corners android:radius="10dp" />
</shape>
</clip>
</item>
</layer-list>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<!-- 滑块颜色:系统蓝色 -->
<solid android:color="@android:color/holo_blue_light" />
<!-- 滑块大小20dp适配小米机型触摸区域 -->
<size
android:width="20dp"
android:height="20dp" />
<!-- 白色边框:区分滑块与进度条 -->
<stroke
android:width="2dp"
android:color="@android:color/white" />
</shape>

View File

@@ -13,138 +13,132 @@
android:gravity="center_vertical"
style="@style/DefaultAToolbar"/>
<LinearLayout
android:orientation="vertical"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="0dp"
android:layout_weight="1.0">
<RelativeLayout
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF28C000">
android:id="@+id/background_view">
</cc.winboll.studio.powerbell.views.BackgroundView>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="400dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Origin BG"
android:id="@+id/activitybackgroundpictureAButton5"
android:layout_alignParentLeft="true"
android:layout_margin="5dp"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Received BG"
android:id="@+id/activitybackgroundpictureAButton4"
android:layout_alignParentRight="true"
android:layout_margin="5dp"/>
</RelativeLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.powerbell.views.BackgroundView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF3243E2"
android:id="@+id/background_view">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="◎"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton1"/>
</cc.winboll.studio.powerbell.views.BackgroundView>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="☑"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton2"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="♾"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton9"
android:onClick="onNetworkBackgroundDialog"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#B92FABE6">
android:layout_height="wrap_content"
android:gravity="right">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton3"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Origin BG"
android:id="@+id/activitybackgroundpictureAButton5"
android:layout_alignParentLeft="true"
android:layout_margin="5dp"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+~]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp"
android:layout_height="36dp"
android:text="Received BG"
android:id="@+id/activitybackgroundpictureAButton4"
android:layout_alignParentRight="true"
android:layout_margin="5dp"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[◐]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton7"/>
</RelativeLayout>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[©]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundsettingsAButton1"
android:onClick="onColorPaletteDialog"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="◎"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton1"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="☑"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton2"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="♾"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton9"
android:onClick="onNetworkBackgroundDialog"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton3"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[+~]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[◐]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton7"/>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[○]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton8"/>
</LinearLayout>
<cc.winboll.studio.libaes.views.AButton
android:layout_width="50dp"
android:layout_height="36dp"
android:text="[○]"
android:layout_gravity="center_vertical"
android:layout_margin="5dp"
android:id="@+id/activitybackgroundpictureAButton8"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -25,11 +25,11 @@
android:layout_height="wrap_content"
android:gravity="center_vertical|center_horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:id="@+id/dialogbackgroundpicturepreviewImageView1"/>
<cc.winboll.studio.powerbell.views.BackgroundView
android:orientation="vertical"
android:layout_width="200dp"
android:layout_height="200dp"
android:id="@+id/backgroundview"/>
</LinearLayout>

View File

@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
android:background="#FFFFFFFF">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/iv_color_picker"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0000"
android:clickable="true"
android:focusable="true"/>
<ImageView
android:id="@+id/iv_color_scaler"
android:layout_width="100dp"
android:layout_height="100dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/color_scale_logo"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RGB"
android:textSize="16sp"/>
<EditText
android:id="@+id/et_r"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:hint="R"
android:inputType="number"
android:gravity="center"
android:maxLength="3"/>
<EditText
android:id="@+id/et_g"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:hint="G"
android:inputType="number"
android:gravity="center"
android:maxLength="3"/>
<EditText
android:id="@+id/et_b"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:hint="B"
android:inputType="number"
android:gravity="center"
android:maxLength="3"/>
</LinearLayout>
<EditText
android:id="@+id/et_color_value"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="#AARRGGBB"
android:inputType="text"
android:gravity="center"
android:maxLength="9"
android:layout_marginBottom="15dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="15dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="透明度:"
android:textSize="16sp"/>
<TextView
android:id="@+id/tv_alpha_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="100%"
android:textSize="16sp"
android:layout_marginLeft="10dp"/>
</LinearLayout>
<SeekBar
android:id="@+id/sb_alpha"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progress="100"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_marginBottom="20dp">
<TextView
android:id="@+id/tv_brightness_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="-"
android:textSize="20sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"/>
<TextView
android:id="@+id/tv_brightness_value"
android:layout_width="80dp"
android:layout_height="40dp"
android:text="100%"
android:textSize="16sp"
android:gravity="center"/>
<TextView
android:id="@+id/tv_brightness_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:textSize="20sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal">
<TextView
android:id="@+id/tv_cancel"
android:layout_width="100dp"
android:layout_height="45dp"
android:text="取消"
android:textSize="16sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"
android:layout_marginRight="20dp"/>
<TextView
android:id="@+id/tv_confirm"
android:layout_width="100dp"
android:layout_height="45dp"
android:text="确认"
android:textSize="16sp"
android:gravity="center"
android:background="@drawable/btn_common"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="10dp">
</LinearLayout>

View File

@@ -58,9 +58,73 @@
<color name="colorUsege">@color/colorRed</color>
<color name="colorCurrent">@color/colorBlue</color>
<color name="colorCharge">@color/colorYellow</color>
<!--CustomSlideToUnlockView控件配置-->
<color name="colorCustomSlideToUnlockViewWhite">#FFFFFFFF</color>
<!---->
<!-- ============== 基础黑白(必含,适配文字/背景) ============== -->
<color name="white">#FFFFFF</color> <!-- 纯白色(文字/背景) -->
<color name="black">#000000</color> <!-- 近黑色(重要文字) -->
<!-- ============== 基础色系(按钮/强调色常用) ============== -->
<!-- 蓝色系(常用:确认/链接/主题色) -->
<color name="blue_light">#4A90E2</color> <!-- 浅蓝(次要按钮) -->
<color name="blue_normal">#2196F3</color> <!-- 标准蓝(主题/确认按钮) -->
<color name="blue_dark">#1976D2</color> <!-- 深蓝(按压态/重要强调) -->
<!-- 绿色系(常用:成功/完成/安全提示) -->
<color name="green_light">#66BB6A</color> <!-- 浅绿(次要成功态) -->
<color name="green_normal">#4CAF50</color> <!-- 标准绿(成功按钮/提示) -->
<color name="green_dark">#388E3C</color> <!-- 深绿(按压态/重要成功) -->
<!-- 红色系(常用:错误/警告/删除按钮) -->
<color name="red_light">#EF5350</color> <!-- 浅红(次要错误提示) -->
<color name="red_normal">#F44336</color> <!-- 标准红(删除/错误按钮) -->
<color name="red_dark">#D32F2F</color> <!-- 深红(按压态/重要错误) -->
<!-- 黄色系(常用:警告/提醒/高亮) -->
<color name="yellow_light">#FFF59D</color> <!-- 浅黄(次要提醒) -->
<color name="yellow_normal">#FFC107</color> <!-- 标准黄(警告提示/高亮) -->
<color name="yellow_dark">#FFA000</color> <!-- 深黄(重要警告) -->
<!-- 橙色系(常用:提醒/进度/活力色) -->
<color name="orange_normal">#FF9800</color> <!-- 标准橙(提醒按钮/进度) -->
<!-- 紫色系(常用:特殊强调/个性按钮) -->
<color name="purple_normal">#9C27B0</color> <!-- 标准紫(特殊功能按钮) -->
<!-- ============== 透明色(遮罩/背景叠加) ============== -->
<color name="transparent">#00000000</color> <!-- 全透明 -->
<color name="black_transparent_50">#80000000</color> <!-- 50%透明黑(遮罩) -->
<!-- 1. 不透明灰色(常用深浅梯度,直接用) -->
<color name="gray_100">#F5F5F5</color> <!-- 极浅灰(接近白色,背景用) -->
<color name="gray_200">#EEEEEE</color> <!-- 浅灰(卡片/分割线背景) -->
<color name="gray_300">#E0E0E0</color> <!-- 中浅灰(边框/次要背景) -->
<color name="gray_400">#BDBDBD</color> <!-- 中灰(次要文字/图标) -->
<color name="gray_500">#9E9E9E</color> <!-- 标准中灰(常用辅助文字) -->
<color name="gray_600">#757575</color> <!-- 中深灰(常规辅助文字) -->
<color name="gray_700">#616161</color> <!-- 深灰(重要辅助文字) -->
<color name="gray_800">#424242</color> <!-- 极深灰(接近黑色,标题副文本) -->
<color name="gray_900">#212121</color> <!-- 近黑色(特殊场景用) -->
<!-- 2. 半透明灰色(带透明度,遮罩/蒙层用) -->
<color name="gray_transparent_30">#4D9E9E9E</color> <!-- 30%透明中灰A=4D -->
<color name="gray_transparent_50">#809E9E9E</color> <!-- 50%透明中灰A=80 -->
<color name="gray_transparent_70">#B39E9E9E</color> <!-- 70%透明中灰A=B3 -->
<color name="gray_light">#EEE</color> <!-- 等价 #EEEEEE浅灰 -->
<color name="gray_mid">#999</color> <!-- 等价 #999999中灰 -->
<color name="gray_dark">#666</color> <!-- 等价 #666666深灰 -->
<color name="gray_black">#333</color> <!-- 等价 #333333极深灰 -->
<!-- 50% 透明中灰(弹窗遮罩常用) -->
<color name="mask_gray">#809E9E9E</color>
<!-- 30% 透明深灰(背景叠加) -->
<color name="bg_overlay_gray">#4D424242</color>
<!-- 1. 常规灰色(按钮默认态,常用中灰) -->
<color name="btn_gray_normal">#9E9E9E</color>
<!-- 2. 按压深色(按钮点击态,加深一级,提升交互感) -->
<color name="btn_gray_pressed">#757575</color>
<!-- 3. 禁用灰色(按钮不可点击态,浅灰) -->
<color name="btn_gray_disabled">#E0E0E0</color>
</resources>

View File

@@ -26,4 +26,18 @@
<item name="android:textSize">@dimen/text_subtitle_size</item>
</style>
<!-- 自定义调色板对话框样式 -->
<style name="CustomDialogStyle" parent="@android:style/Theme.Dialog">
<!-- 去除标题栏 -->
<item name="android:windowNoTitle">true</item>
<!-- 背景透明(避免小米机型弹窗周围黑边) -->
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 禁止弹窗全屏 -->
<item name="android:windowFullscreen">false</item>
<!-- 小米机型适配:弹窗大小自适应 -->
<item name="android:windowContentOverlay">@null</item>
<!-- 禁止触摸外部关闭(可选,避免误触) -->
<item name="android:windowCloseOnTouchOutside">false</item>
</style>
</resources>