Compare commits

..

20 Commits

Author SHA1 Message Date
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
28 changed files with 2492 additions and 700 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sun Dec 14 04:55:45 HKT 2025 #Tue Dec 16 17:52:54 HKT 2025
stageCount=3 stageCount=8
libraryProject= libraryProject=
baseVersion=15.14 baseVersion=15.14
publishVersion=15.14.2 publishVersion=15.14.7
buildCount=0 buildCount=0
baseBetaVersion=15.14.3 baseBetaVersion=15.14.8

View File

@@ -4,34 +4,56 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="cc.winboll.studio.powerbell"> package="cc.winboll.studio.powerbell">
<!-- 运行前台服务 --> <!-- ====================== 原有权限保留 + 补充核心权限 ====================== -->
<!-- 运行前台服务(原有保留,补充 Android 12+ 特殊前台服务权限) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- 开机启动 --> <!-- 开机启动(原有保留,确保自启广播生效) -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- 显示通知 --> <!-- 显示通知(原有保留,前台服务/保活必备) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- PACKAGE_USAGE_STATS --> <!-- 应用使用统计相关(原有保留,忽略保护权限警告) -->
<uses-permission android:name="android.permission.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.BATTERY_STATS"/>
<uses-permission
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions"/>
<uses-feature android:name="android.hardware.camera"/> <!-- 相机相关(原有保留,补充权限等级声明,避免安装警告) -->
<uses-feature
<uses-feature android:name="android.hardware.camera.autofocus"/> 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-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/> tools:ignore="QueryAllPackagesPermission"/>
<uses-permission <!-- 新增:文件管理权限(对应 PermissionUtils 全文件管理逻辑) -->
android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
tools:ignore="ProtectedPermissions"/> <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 <application
android:name=".App" android:name=".App"
@@ -43,17 +65,20 @@
android:resizeableActivity="true" android:resizeableActivity="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning"> android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<!-- ====================== 页面配置(原有保留,优化 exported 安全) ====================== -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:exported="true" android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
</activity> </activity>
<activity android:name=".activities.CrashActivity"/> <activity
android:name=".activities.CrashActivity"
android:exported="false"/> <!-- 新增:非外部调用,设为 false提升安全 -->
<activity-alias <activity-alias
android:name=".MainActivityEN1" android:name=".MainActivityEN1"
@@ -62,19 +87,13 @@
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:enabled="true"> android:enabled="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmainen1"/> android:resource="@xml/shortcutsmainen1"/>
</activity-alias> </activity-alias>
<activity-alias <activity-alias
@@ -84,19 +103,13 @@
android:label="@string/app_name_cn1" android:label="@string/app_name_cn1"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:enabled="false"> android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn1"/> android:resource="@xml/shortcutsmaincn1"/>
</activity-alias> </activity-alias>
<activity-alias <activity-alias
@@ -106,116 +119,121 @@
android:label="@string/app_name_cn2" android:label="@string/app_name_cn2"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:enabled="false"> android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcutsmaincn2"/> android:resource="@xml/shortcutsmaincn2"/>
</activity-alias> </activity-alias>
<activity <activity
android:name=".activities.ClearRecordActivity" android:name=".activities.ClearRecordActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity" android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:launchMode="singleTask"> android:launchMode="singleTask"
android:exported="false"/> <!-- 新增:非外部调用,设为 false -->
</activity>
<activity <activity
android:name=".activities.BackgroundSettingsActivity" android:name=".activities.BackgroundSettingsActivity"
android:parentActivityName="cc.winboll.studio.powerbell.MainActivity" android:parentActivityName="cc.winboll.studio.powerbell.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/jpeg"/> <data android:mimeType="image/jpeg"/>
<data android:mimeType="image/jpg"/> <data android:mimeType="image/jpg"/>
<data android:mimeType="image/png"/> <data android:mimeType="image/png"/>
<data android:mimeType="image/webp"/> <data android:mimeType="image/webp"/>
<data android:mimeType="image/*"/> <data android:mimeType="image/*"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- ====================== 广播接收器(优化自启广播,提升保活成功率) ====================== -->
<receiver <receiver
android:name=".receivers.MainReceiver" android:name=".receivers.MainReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="true"
android:directBootAware="true"> android:directBootAware="true">
<intent-filter android:priority="1000"> <!-- 新增:广播优先级最高,确保优先接收开机广播 -->
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <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> </intent-filter>
</receiver> </receiver>
<!-- ====================== 服务配置(核心优化:前台服务保活,适配 API29-30 ====================== -->
<!-- 核心前台服务ControlCenterService保活核心重点优化 -->
<service <service
android:name=".services.ControlCenterService" android:name=".services.ControlCenterService"
android:priority="1000" android:priority="1000"
android:enabled="true" android:enabled="true"
android:exported="false" 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 <service
android:name=".services.AssistantService" android:name=".services.AssistantService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:process=".assistantservice"/> android:process=".assistantservice"> <!-- 若需前台启动,添加此配置;纯后台可移除 -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FOREGROUND_SERVICE"
android:value="辅助核心功能运行" />
</service>
<!-- ====================== 其他配置(原有保留,补充优化) ====================== -->
<meta-data <meta-data
android:name="android.max_aspect" android:name="android.max_aspect"
android:value="4.0"/> android:value="4.0"/>
<activity android:name=".activities.BatteryReporterActivity"/> <!-- 所有非外部调用的 Activity统一设 exported=false提升安全 -->
<activity
<activity android:name=".activities.PixelPickerActivity"/> android:name=".activities.BatteryReporterActivity"
android:exported="false"/>
<activity android:name=".activities.BatteryReportActivity"/> <activity
android:name=".activities.PixelPickerActivity"
<activity android:name=".unittest.MainUnitTestActivity"/> 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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider"/> android:resource="@xml/file_provider"/>
</provider> </provider>
<activity android:name=".activities.ShortcutActionActivity"/> <!-- UCrop 第三方页面原有保留exported=true 正常) -->
<activity android:name=".activities.SettingsActivity"/>
<activity <activity
android:name="com.yalantis.ucrop.UCropActivity" android:name="com.yalantis.ucrop.UCropActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="true"> android:exported="true">
</activity> </activity>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -129,20 +129,7 @@ public class MainActivity extends WinBoLLActivity {
@Override @Override
protected void onPostCreate(Bundle savedInstanceState) { protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
permissionUtils.startPermissionRequest(this);
// 电池优化权限(通用所有机型)
if (!permissionUtils.checkIgnoreBatteryOptimizationPermission(this)) {
YesNoAlertDialog.show(this, getString(R.string.app_name) + "权限申请提示:", "本应用要正常使用,需要申请电池优化与自启动权限。是否进入权限设置步骤?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onNo() {
ToastUtils.show(getString(R.string.app_name) + "应用可能无法正常使用。");
}
@Override
public void onYes() {
permissionUtils.requestIgnoreBatteryOptimizationPermission(MainActivity.this);
}
});
}
} }
@Override @Override
@@ -226,13 +213,9 @@ public class MainActivity extends WinBoLLActivity {
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PermissionUtils.REQUEST_IGNORE_BATTERY_OPTIMIZATION) { permissionUtils.handlePermissionRequest(this, requestCode, resultCode, data);
// 自启动权限(小米专属)
if (permissionUtils.checkAutoStartPermission(this)) { if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
// 小米机型,发起自启动权限申请
permissionUtils.requestAutoStartPermission(this);
}
} else if (requestCode == REQUEST_READ_MEDIA_IMAGES) {
if (_mHandler != null) { if (_mHandler != null) {
_mHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND); _mHandler.sendEmptyMessage(MSG_LOAD_BACKGROUND);
} }

View File

@@ -3,7 +3,6 @@ package cc.winboll.studio.powerbell.activities;
import android.app.Activity; import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
@@ -16,7 +15,6 @@ import android.os.Looper;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
@@ -25,6 +23,7 @@ import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.App;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; 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.dialogs.YesNoAlertDialog;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
@@ -33,6 +32,7 @@ import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils; import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.utils.UriUtils; import cc.winboll.studio.powerbell.utils.UriUtils;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -208,6 +208,7 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener); findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener);
findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener);
findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener);
findViewById(R.id.activitybackgroundsettingsAButton1).setOnClickListener(onColorPaletteClickListener);
} }
// ====================== 按钮点击事件 ====================== // ====================== 按钮点击事件 ======================
@@ -331,6 +332,32 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
} }
}; };
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+ * 生成 FileProvider Uri适配 Android 7.0+
@@ -830,31 +857,32 @@ public class BackgroundSettingsActivity extends WinBoLLActivity {
previewBean.setIsUseBackgroundFile(true); previewBean.setIsUseBackgroundFile(true);
previewBean.setIsUseBackgroundScaledCompressFile(true); previewBean.setIsUseBackgroundScaledCompressFile(true);
mBgSourceUtils.saveSettings(); mBgSourceUtils.saveSettings();
doubleRefreshPreview();
float systemFileRatio = getRatioFromSystemCropFile(cropTempFile); // float systemFileRatio = getRatioFromSystemCropFile(cropTempFile);
if (systemFileRatio > 0) { // if (systemFileRatio > 0) {
Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); // Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile);
if (isBitmapValid(cropBitmap)) { // if (isBitmapValid(cropBitmap)) {
Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio); // Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio);
if (isBitmapValid(scaledCropBitmap)) { // if (isBitmapValid(scaledCropBitmap)) {
saveScaledBitmapToFile(scaledCropBitmap, cropTempFile); // saveScaledBitmapToFile(scaledCropBitmap, cropTempFile);
scaledCropBitmap.recycle(); // scaledCropBitmap.recycle();
} // }
cropBitmap.recycle(); // cropBitmap.recycle();
} else { // } else {
LogUtils.e(TAG, "【裁剪结果】裁剪Bitmap解析无效"); // LogUtils.e(TAG, "【裁剪结果】裁剪Bitmap解析无效");
} // }
} // }
//
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { // new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override // @Override
public void run() { // public void run() {
if (!isFinishing()) { // if (!isFinishing()) {
doubleRefreshPreview(); // doubleRefreshPreview();
LogUtils.d(TAG, "【裁剪结果】触发双重刷新"); // LogUtils.d(TAG, "【裁剪结果】触发双重刷新");
} // }
} // }
}, 300); // }, 300);
} else { } else {
handleOperationCancelOrFail(); handleOperationCancelOrFail();
} }

View File

@@ -0,0 +1,732 @@
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");
openColorPickerDialog(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 openColorPickerDialog(int nColor){
//ToastUtils.show("openColorPickerDialog");
ColorPickerDialog dlg = new ColorPickerDialog(getContext(), nColor);
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int 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;
}
}
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
}
}

View File

@@ -1,183 +1,249 @@
package cc.winboll.studio.powerbell.unittest; package cc.winboll.studio.powerbell.unittest;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity; 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.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.MainActivity;
import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils;
import cc.winboll.studio.powerbell.utils.FileUtils;
import cc.winboll.studio.powerbell.utils.ImageCropUtils; import cc.winboll.studio.powerbell.utils.ImageCropUtils;
import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BackgroundView;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import cc.winboll.studio.powerbell.models.BackgroundBean;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * 终极修复版放弃FileProvider直接用私有目录File路径彻底解决UID冲突
* @Date 2025/11/19 18:04
* @Describe 单元测试启动主页窗口
*/ */
public class MainUnitTestActivity extends AppCompatActivity { public class MainUnitTestActivity extends AppCompatActivity {
// ====================== 常量定义 ======================
public static final String TAG = "MainUnitTestActivity"; public static final String TAG = "MainUnitTestActivity";
public static final int REQUEST_CROP_IMAGE = 0; public static final int REQUEST_CROP_IMAGE = 0;
// 新增:权限请求码 private static final String ASSETS_TEST_IMAGE_PATH = "unittest/unittest-miku.png";
public static final int REQUEST_STORAGE_PERMISSION = 1001;
View mainView; // ====================== 成员变量移除所有Uri相关 ======================
BackgroundSourceUtils mBgSourceUtils; private BackgroundView mBackgroundView;
BackgroundView mBackgroundView; private String mAppPrivateDirPath;
// 测试图片路径用Environment获取适配低版本避免硬编码 private File mPrivateTestImageFile; // 仅用File不用Uri
String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) private File mPrivateCropImageFile;
+ "/PowerBell/unittest/1764946782079.jpeg"; BackgroundBean mPreviewBackgroundBean;
// ====================== 生命周期方法 ======================
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
LogUtils.d(TAG, "=== 页面 onCreate 启动 ===");
mBgSourceUtils = BackgroundSourceUtils.getInstance(this);
mBgSourceUtils.loadSettings();
setContentView(R.layout.activity_mainunittest);
mBackgroundView = findViewById(R.id.backgroundview);
((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){ initBaseParams();
@Override initViewAndEvent();
public void onClick(View v) { copyAssetsTestImageToPrivateDir();
startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class)); //loadBackgroundByFile(); // 直接用File加载
} mPreviewBackgroundBean = new BackgroundBean();
}); mPreviewBackgroundBean.setBackgroundFileName(mPrivateTestImageFile.getName());
mPreviewBackgroundBean.setBackgroundFilePath(mPrivateTestImageFile.getAbsolutePath());
// 裁剪测试按钮点击事件(新增权限校验) mPreviewBackgroundBean.setBackgroundScaledCompressFileName(mPrivateCropImageFile.getName());
((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){ mPreviewBackgroundBean.setBackgroundScaledCompressFilePath(mPrivateCropImageFile.getAbsolutePath());
@Override mPreviewBackgroundBean.setIsUseBackgroundFile(true);
public void onClick(View v) { doubleRefreshPreview();
ToastUtils.show("onClick准备启动裁剪");
LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限");
// 修复1移除高版本API依赖适配低版本存储权限校验
if (checkStoragePermission()) {
// 权限已授予,启动裁剪
startCropTest();
} else {
// 权限未授予,申请权限
requestStoragePermission();
}
}
});
ToastUtils.show(String.format("%s onCreate", TAG)); ToastUtils.show("单元测试页面启动完成");
// 加载测试图片(验证图片路径是否有效) LogUtils.d(TAG, "=== 页面 onCreate 初始化结束 ===");
loadBackground();
} }
/**
* 启动裁剪测试(抽取为单独方法,便于权限回调后调用)
*/
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 @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_STORAGE_PERMISSION) { LogUtils.d(TAG, "=== onActivityResult 回调 ===");
// 校验权限是否授予 if (requestCode == REQUEST_CROP_IMAGE) {
boolean allGranted = true; handleCropResult(resultCode);
for (int result : grantResults) { }
if (result != PackageManager.PERMISSION_GRANTED) { }
allGranted = false;
break; // ====================== 初始化相关方法 ======================
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 // ====================== 核心业务方法全改为File路径 ======================
protected void onActivityResult(int requestCode, int resultCode, Intent data) { /** 直接用File路径加载背景图无Uri无冲突 */
super.onActivityResult(requestCode, resultCode, data); // private void loadBackgroundByFile() {
LogUtils.d(TAG, "【裁剪回调】requestCode" + requestCode + "resultCode" + resultCode + "data" + (data == null ? "null" : data.toString())); // LogUtils.d(TAG, "开始加载背景图File路径版");
ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null)); // if (mPrivateTestImageFile.exists() && mPrivateTestImageFile.length() > 100) {
// 裁剪完成后回收权限 // mBackgroundView.loadImage(mPrivateTestImageFile.getAbsolutePath()); // 直接传路径
if (requestCode == REQUEST_CROP_IMAGE) { // LogUtils.d(TAG, "背景图加载成功:" + mPrivateTestImageFile.getAbsolutePath());
String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) // ToastUtils.show("背景图加载成功");
+ "/PowerBell/unittest/1764946782079-crop.jpeg"; // } else {
//Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath)); // LogUtils.e(TAG, "背景图加载失败:文件无效");
//ImageCropUtils.releaseCropPermission(this, outputUri); // ToastUtils.show("背景图加载失败");
mBackgroundView.loadImage(dstOutputPath); // }
// }
/** 直接用File启动裁剪关键调用ImageCropUtils的File重载方法 */
private void startCropTestByFile() {
LogUtils.d(TAG, "启动裁剪File路径版原图" + mPrivateTestImageFile.getAbsolutePath());
// 确保输出目录存在
File cropParent = mPrivateCropImageFile.getParentFile();
if (!cropParent.exists()) {
cropParent.mkdirs();
} }
}
// 调用ImageCropUtils的File参数方法核心绕开Uri
void loadBackground() { ImageCropUtils.startImageCrop(
// 校验测试图片是否存在(避免路径错误) this,
File testFile = new File(szTestSource); mPrivateTestImageFile, // 原图File
if (testFile.exists() && testFile.length() > 100) { mPrivateCropImageFile, // 输出File
mBackgroundView.loadImage(szTestSource); 0,
LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource); 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 { } else {
ToastUtils.show("测试图片不存在或无效"); LogUtils.e(TAG, "裁剪失败resultCode异常");
LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource); 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

@@ -211,7 +211,7 @@ public class BackgroundSourceUtils {
LogUtils.d(TAG, "背景Bean文件存在无需创建空白背景"); LogUtils.d(TAG, "背景Bean文件存在无需创建空白背景");
return false; return false;
} }
String genNewCropFileName() { String genNewCropFileName() {
return UUID.randomUUID().toString() + System.currentTimeMillis(); return UUID.randomUUID().toString() + System.currentTimeMillis();
} }
@@ -231,10 +231,12 @@ public class BackgroundSourceUtils {
} }
Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath()); Uri uri = UriUtils.getUriForFile(mContext, oldPreviewBackgroundBean.getBackgroundFilePath());
String fileSuffix = FileUtils.getFileSuffix(mContext, uri); 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(); String newCropFileName = genNewCropFileName();
mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); 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())) { if (FileUtils.isFileExists(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath())) {
FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile); FileUtils.copyFile(new File(oldPreviewBackgroundBean.getBackgroundScaledCompressFilePath()), mCropResultFile);

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) { public static boolean isFileExists(String path) {
File file = new File(path); File file = new File(path);
return file.exists(); 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 android.os.Build;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.powerbell.R;
import cc.winboll.studio.powerbell.models.BackgroundBean; import cc.winboll.studio.powerbell.models.BackgroundBean;
import com.yalantis.ucrop.UCrop; import com.yalantis.ucrop.UCrop;
import com.yalantis.ucrop.UCropActivity;
import java.io.File; import java.io.File;
import cc.winboll.studio.powerbell.R;
/** /**
* 图片裁剪工具类集成uCrop,脱离系统依赖 * 图片裁剪工具类(集成 uCrop 2.2.8 终极兼容版,强制输出 PNG 格式,全程保留透明通道,支持 Uri/File 双传参
*/ */
public class ImageCropUtils { public class ImageCropUtils {
public static final String TAG = "ImageCropUtils"; public static final String TAG = "ImageCropUtils";
// FileProvider 授权(与项目一致) // FileProvider 授权(与 AndroidManifest 配置一致)
private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; 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 activity 上下文
* @param inputFile 输入图片文件 * @param inputFile 输入图片文件(任意格式)
* @param outputFile 输出图片文件 * @param outputFile 输出图片文件(最终强制转为 PNG
* @param isFreeCrop 是否自由裁剪true=自由false=固定比例 * @param aspectX 固定比例 X自由裁剪传 0
* @param aspectY 固定比例 Y自由裁剪传 0
* @param isFreeCrop 是否自由裁剪
* @param requestCode 裁剪请求码 * @param requestCode 裁剪请求码
*/ */
public static void startImageCrop(Activity activity, public static void startImageCrop(Activity activity,
File inputFile, File inputFile,
File outputFile, File outputFile,
int aspectX, int aspectX,
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
// 校验输入参数 // 1. 输入参数校验
if (activity == null || activity.isFinishing()) { if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "【裁剪异常】上下文Activity无效"); LogUtils.e(TAG, "【裁剪异常】Activity 无效或已销毁");
return; return;
} }
if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) { if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) {
LogUtils.e(TAG, "【裁剪异常】输入文件无效"); LogUtils.e(TAG, "【裁剪异常】输入图片文件无效");
showToast(activity, "无有效图片可裁剪"); showToast(activity, "无有效图片可裁剪");
return; return;
} }
@@ -51,47 +110,28 @@ public class ImageCropUtils {
return; return;
} }
// 生成输入/输出Uri适配FileProvider // 2. 核心:强制修正输出为 PNG忽略原图格式
Uri inputUri = getFileProviderUri(activity, inputFile); 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 uCrop = UCrop.of(inputUri, outputUri);
UCrop.Options options = new UCrop.Options(); uCrop.withAspectRatio(aspectX, aspectY);
UCrop.Options options = initCropOptions(activity, isFreeCrop, aspectX, aspectY); // 移除 isPng 参数
// 裁剪模式配置(自由裁剪/固定比例) // 4. 启动裁剪
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)); // 状态栏颜色
// 应用配置并启动裁剪
uCrop.withOptions(options); uCrop.withOptions(options);
// 启动uCrop裁剪Activity替代系统裁剪
uCrop.start(activity, requestCode); uCrop.start(activity, requestCode);
LogUtils.d(TAG, "【裁剪启动成功File 版)】强制输出 PNG透明保留输出路径" + outputFile.getAbsolutePath());
LogUtils.d(TAG, "【uCrop启动】成功输入Uri" + inputUri + "输出Uri" + outputUri + ",请求码:" + requestCode);
} }
/** /**
* 重载方法:适配BackgroundBean * BackgroundBean 传参版】启动 uCrop 裁剪 - 强制输出 PNG保留透明通道
*/ */
public static void startImageCrop(Activity activity, public static void startImageCrop(Activity activity,
BackgroundBean cropBean, BackgroundBean cropBean,
int aspectX, int aspectX,
int aspectY, int aspectY,
boolean isFreeCrop, boolean isFreeCrop,
int requestCode) { int requestCode) {
@@ -100,70 +140,163 @@ public class ImageCropUtils {
startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode); startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode);
} }
/** // ====================== 裁剪结果处理(保持兼容,优化日志)======================
* 生成FileProvider Uri public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) {
*/ if (requestCode != cropRequestCode) return null;
private static Uri getFileProviderUri(Activity activity, File file) {
try { if (resultCode == Activity.RESULT_OK && data != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Uri outputUri = UCrop.getOutput(data);
String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX; if (outputUri != null) {
Uri uri = FileProvider.getUriForFile(activity, authority, file); String outputPath = uriToPath(outputUri);
LogUtils.d(TAG, "Uri生成】FileProvider Uri" + uri); LogUtils.d(TAG, "裁剪成功】强制输出 PNG透明保留输出路径" + outputPath);
return uri; return outputPath;
} else {
Uri uri = Uri.fromFile(file);
LogUtils.d(TAG, "【Uri生成】普通Uri" + uri);
return uri;
} }
} 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) { } 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; return null;
} }
} }
/** /**
* 处理uCrop裁剪回调在Activity的onActivityResult中调用 * 统一初始化裁剪配置(强制 PNG 专属配置,保留透明核心
* @param requestCode 请求码 * 移除 isPng 参数,全程用 PNG 配置
* @param resultCode 结果码
* @param data 回调数据
* @return 裁剪成功返回输出文件路径失败返回null
*/ */
public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) { private static UCrop.Options initCropOptions(Activity activity, boolean isFreeCrop, int aspectX, int aspectY) {
// 校验是否是uCrop的回调
if (requestCode == cropRequestCode) { UCrop.Options options = new UCrop.Options();
if (resultCode == Activity.RESULT_OK && data != null) {
// 裁剪成功获取输出Uri // 裁剪模式配置(自由裁剪/固定比例)
Uri outputUri = UCrop.getOutput(data); options.setFreeStyleCropEnabled(isFreeCrop); // 开启自由裁剪
if (outputUri != null) {
String outputPath = outputUri.getPath(); // 裁剪配置(优化体验)
LogUtils.d(TAG, "【uCrop回调】裁剪成功输出路径" + outputPath); //options.setCompressionFormat(android.graphics.Bitmap.CompressFormat.JPEG); // 输出格式
return outputPath; //options.setCompressionQuality(100); // 图片质量
} //options.setHideBottomControls(true); // 隐藏底部控制栏(简化界面)
} else if (resultCode == UCrop.RESULT_ERROR) { //options.setToolbarTitle("图片裁剪"); // 工具栏标题
// 裁剪失败,获取异常信息 //options.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary)); // 工具栏颜色(适配项目主题)
Throwable error = UCrop.getError(data); //options.setStatusBarColor(activity.getResources().getColor(R.color.colorPrimaryDark)); // 状态栏颜色
LogUtils.e(TAG, "【uCrop回调】裁剪失败" + (error != null ? error.getMessage() : "未知错误"));
} else {
LogUtils.d(TAG, "【uCrop回调】裁剪被取消"); // 2. 核心:强制 PNG 保留透明(固定配置,无需判断原图格式)
} options.setCompressionFormat(FORCE_COMPRESS_FORMAT); // 强制 PNG 压缩
} options.setCompressionQuality(100); // PNG 100% 质量,不损失透明
return null; 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) { private static void showToast(Activity activity, String msg) {
if (activity != null && !activity.isFinishing()) { if (activity != null && !activity.isFinishing()) {
android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show(); android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show();
} }
} }
/** // ====================== 公有辅助方法(供外部调用)======================
* 暴露getFileProviderUri方法供外部调用
*/
public static Uri getFileProviderUriPublic(Activity activity, File file) { public static Uri getFileProviderUriPublic(Activity activity, File file) {
return getFileProviderUri(activity, 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

@@ -5,31 +5,43 @@ import android.app.AlertDialog;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Environment;
import android.os.PowerManager; import android.os.PowerManager;
import android.provider.Settings; 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.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;
/** /**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/12/14 03:05 * @Date 2025/12/14 03:05
* @Describe 权限申请工具类Java7兼容版 * @Describe 权限申请工具类Java7兼容版
* 适配 小米手机+API30专注自启动权限、电池优化权限检查与申请 * 适配 小米手机+API29-30整合自启动、电池优化、全文件管理权限,专注后台保活核心权限
*/ */
public class PermissionUtils { public class PermissionUtils {
// ====================== 常量定义(统一管理,首屏可见====================== // ====================== 常量定义(首屏可见,统一管理,避免冲突======================
// 日志标签
public static final String TAG = "PermissionUtils"; public static final String TAG = "PermissionUtils";
// 权限请求码(仅保留核心权限场景 // 权限请求码(按场景分段,避免重复
public static final int REQUEST_IGNORE_BATTERY_OPTIMIZATION = 1000; // 电池优化权限 public static final int REQUEST_IGNORE_BATTERY_OPTIMIZATION = 1000; // 电池优化权限
public static final int REQUEST_AUTO_START = 1001; // 自启动权限(小米专属) public static final int REQUEST_AUTO_START = 1001; // 自启动权限(小米专属)
// SDK版本常量适配API30替代系统枚举 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 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_PACKAGE = "com.miui.securitycenter";
private static final String XIAOMI_AUTO_START_CLASS = "com.miui.permcenter.autostart.AutoStartManagementActivity"; private static final String XIAOMI_AUTO_START_CLASS = "com.miui.permcenter.autostart.AutoStartManagementActivity";
// ====================== 单例模式Java7标准双重校验锁====================== // ====================== 单例模式Java7标准双重校验锁,线程安全+懒加载======================
private static volatile PermissionUtils sInstance; private static volatile PermissionUtils sInstance;
private PermissionUtils() {} private PermissionUtils() {}
@@ -39,81 +51,148 @@ public class PermissionUtils {
synchronized (PermissionUtils.class) { synchronized (PermissionUtils.class) {
if (sInstance == null) { if (sInstance == null) {
sInstance = new PermissionUtils(); sInstance = new PermissionUtils();
LogUtils.d(TAG, "【单例初始化PermissionUtils 例创建成功"); LogUtils.d(TAG, "初始化PermissionUtils 例创建成功");
} }
} }
} }
return sInstance; return sInstance;
} }
// ====================== 自启动权限(拆分检查+请求,小米专属====================== // ====================== 核心权限1全文件管理权限API29-30适配通用所有机型======================
/** /**
* 检查是否拥有自启动权限小米手机专属判断API30适配 * 检查全文件管理权限适配API30+ MANAGE_EXTERNAL_STORAGE兼容API29-旧权限
* 注小米自启动无系统API直接校验通过「是否为小米机型」+「功能场景间接判断」,此处返回机型适配状态
* @param activity 上下文Activity不可为null * @param activity 上下文Activity不可为null
* @return true小米机型需手动开启权限false非小米机型无需申请 * @return true=权限已授予false=权限未授予
*/ */
public boolean checkAutoStartPermission(Activity activity) { public boolean checkAllFileManagePermission(Activity activity) {
LogUtils.d(TAG, "全文件权限-检查:开始校验,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null) { if (activity == null) {
LogUtils.e(TAG, "【自启动权限-检查失败Activity为空"); LogUtils.e(TAG, "全文件权限-检查失败Activity为空");
return false; return false;
} }
LogUtils.d(TAG, "【自启动权限-检查】开始,设备品牌:" + Build.BRAND + ",系统版本:" + Build.VERSION.SDK_INT);
// 仅小米机型需要申请自启动权限非小米直接返回false无需处理 // API30+:校验 MANAGE_EXTERNAL_STORAGE 特殊权限
boolean isXiaomi = Build.BRAND.toLowerCase().contains("xiaomi"); if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
LogUtils.d(TAG, "【自启动权限-检查】结果:" + (isXiaomi ? "小米机型(需手动开启)" : "非小米机型(无需申请)")); boolean hasManagePerm = Environment.isExternalStorageManager();
return isXiaomi; 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 {
boolean hasWritePerm = ContextCompat.checkSelfPermission(activity,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
LogUtils.d(TAG, "全文件权限-检查API29以下WRITE_EXTERNAL_STORAGE权限=" + (hasWritePerm ? "已授予" : "未授予"));
return hasWritePerm;
}
} }
/** /**
* 请求自启动权限小米手机专属API30适配跳转系统页面引导开启 * 申请全文件管理权限适配API30+特殊权限流程兼容API29-旧权限申请
* @param activity 申请权限的Activity不可为null
*/
public void requestAllFileManagePermission(Activity activity) {
LogUtils.d(TAG, "全文件权限-申请:开始处理,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "全文件权限-申请失败Activity无效/已销毁");
return;
}
// 先检查权限,已授予直接返回
if (checkAllFileManagePermission(activity)) {
LogUtils.d(TAG, "全文件权限-申请:已拥有权限,无需发起");
return;
}
// API30+:跳转系统特殊权限申请页(用户手动授权)
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
try {
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) {
// 备用跳转:系统设置首页,引导手动操作
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 * @param activity 申请权限的Activity不可为null
*/ */
public void requestAutoStartPermission(Activity activity) { public void requestAutoStartPermission(Activity activity) {
if (activity == null) { LogUtils.d(TAG, "自启动权限-申请:开始处理");
LogUtils.e(TAG, "【自启动权限-请求】失败Activity为空"); if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "自启动权限-申请失败Activity无效/已销毁");
return; return;
} }
// 先检查机型,非小米不执行请求
if (!checkAutoStartPermission(activity)) {
LogUtils.d(TAG, "【自启动权限-请求】非小米机型,无需发起请求");
return;
}
LogUtils.d(TAG, "【自启动权限-请求】开始处理,系统版本:" + Build.VERSION.SDK_INT);
// API30+ 小米手机:优先精准跳转自启动管理页 // 非小米机型,直接返回
// if (!checkAutoStartPermission(activity)) {
// LogUtils.d(TAG, "自启动权限-申请:非小米机型,无需处理");
// return;
// }
// API30+ 小米:优先精准跳转自启动管理页
if (Build.VERSION.SDK_INT >= SDK_VERSION_R) { if (Build.VERSION.SDK_INT >= SDK_VERSION_R) {
try { try {
// 方案1组件名精准跳转成功率最高 // 方案1组件名精准跳转成功率最高
Intent intent = new Intent(); Intent intent = new Intent();
intent.setComponent(new ComponentName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS)); intent.setComponent(new ComponentName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS));
activity.startActivityForResult(intent, REQUEST_AUTO_START); activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-请求】跳转小米自启动管理页(组件名跳转)"); LogUtils.d(TAG, "自启动权限-申请API30+,组件名跳转自启动管理页");
} catch (Exception e1) { } catch (Exception e1) {
try { try {
// 方案2Action备用跳转兼容机型差异 // 方案2Action备用跳转兼容机型差异
Intent intent = new Intent("miui.intent.action.OP_AUTO_START"); Intent intent = new Intent("miui.intent.action.OP_AUTO_START");
intent.setClassName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS); intent.setClassName(XIAOMI_AUTO_START_PACKAGE, XIAOMI_AUTO_START_CLASS);
activity.startActivityForResult(intent, REQUEST_AUTO_START); activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-请求】跳转小米自启动管理页(Action跳转"); LogUtils.d(TAG, "自启动权限-申请API30+Action跳转自启动管理页");
} catch (Exception e2) { } catch (Exception e2) {
// 方案3终极备用跳转系统设置,引导手动操作) // 方案3终极备用跳转系统设置+提示
Intent intent = new Intent(Settings.ACTION_SETTINGS); Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START); activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.w(TAG, "自启动权限-请求】跳转系统设置页(引导手动开启)"); LogUtils.w(TAG, "自启动权限-申请:跳转失败,引导手动操作");
showAutoStartTipsDialog(activity); showAutoStartTipsDialog(activity);
} }
} }
return; return;
} }
// API30以下小米手机兼容低版本跳转逻辑 // API29 小米:低版本兼容跳转
LogUtils.d(TAG, "【自启动权限-请求】API30以下小米机型执行低版本跳转");
try { try {
Intent intent = new Intent(XIAOMI_AUTO_START_CLASS); Intent intent = new Intent(XIAOMI_AUTO_START_CLASS);
intent.setPackage(XIAOMI_AUTO_START_PACKAGE); intent.setPackage(XIAOMI_AUTO_START_PACKAGE);
activity.startActivityForResult(intent, REQUEST_AUTO_START); activity.startActivityForResult(intent, REQUEST_AUTO_START);
LogUtils.d(TAG, "自启动权限-申请API29低版本跳转自启动管理页");
} catch (Exception e) { } catch (Exception e) {
Intent intent = new Intent(Settings.ACTION_SETTINGS); Intent intent = new Intent(Settings.ACTION_SETTINGS);
activity.startActivityForResult(intent, REQUEST_AUTO_START); activity.startActivityForResult(intent, REQUEST_AUTO_START);
@@ -121,75 +200,94 @@ public class PermissionUtils {
} }
} }
// ====================== 电池优化权限(拆分检查+请求,通用所有机型)====================== // ====================== 核心权限3电池优化权限(通用所有机型API29-30适配======================
/** /**
* 检查是否拥有「忽略电池优化权限(API30适配通用所有机型精准返回权限状态 * 检查忽略电池优化权限(精准判断API23+有效,低版本视为已拥有
* @param activity 上下文Activity不可为null * @param activity 上下文Activity不可为null
* @return true:已拥有(忽略优化false:未拥有(需申请) * @return true=已忽略优化false=未忽略(需申请)
*/ */
public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) { public boolean checkIgnoreBatteryOptimizationPermission(Activity activity) {
LogUtils.d(TAG, "电池优化权限-检查:开始,系统版本=" + Build.VERSION.SDK_INT);
if (activity == null) { if (activity == null) {
LogUtils.e(TAG, "电池优化权限-检查失败Activity为空"); LogUtils.e(TAG, "电池优化权限-检查失败Activity为空");
return false; return false;
} }
LogUtils.d(TAG, "【电池优化权限-检查】开始,系统版本:" + Build.VERSION.SDK_INT);
// API23以下无电池优化权限,直接视为已拥有 // API23以下无权限,视为已拥有
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
LogUtils.d(TAG, "电池优化权限-检查API23以下无此权限,视为已拥有"); LogUtils.d(TAG, "电池优化权限-检查API23以下,无需校验,视为已拥有");
return true; return true;
} }
// API23+ 精准校验权限状态 // API23+ 精准校验权限状态
PowerManager powerManager = (PowerManager) activity.getSystemService(Activity.POWER_SERVICE); PowerManager powerManager = (PowerManager) activity.getSystemService(Activity.POWER_SERVICE);
if (powerManager == null) { if (powerManager == null) {
LogUtils.e(TAG, "电池优化权限-检查获取PowerManager失败无法校验"); LogUtils.e(TAG, "电池优化权限-检查获取PowerManager失败校验异常");
return false; return false;
} }
boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName()); boolean isIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());
LogUtils.d(TAG, "电池优化权限-检查结果" + (isIgnored ? "拥有(忽略优化" : "拥有(需申请)")); LogUtils.d(TAG, "电池优化权限-检查结果=" + (isIgnored ? "已忽略优化" : "忽略(需申请)"));
return isIgnored; return isIgnored;
} }
/** /**
* 请求忽略电池优化权限(API30适配通用所有机型,自动判断是否需要申请) * 请求忽略电池优化权限(多方案跳转适配API29-30,自动判断是否需要申请)
* @param activity 申请权限的Activity不可为null * @param activity 申请权限的Activity不可为null
*/ */
public void requestIgnoreBatteryOptimizationPermission(Activity activity) { public void requestIgnoreBatteryOptimizationPermission(Activity activity) {
if (activity == null) { LogUtils.d(TAG, "电池优化权限-申请:开始处理");
LogUtils.e(TAG, "【电池优化权限-请求】失败Activity为空"); if (activity == null || activity.isFinishing()) {
LogUtils.e(TAG, "电池优化权限-申请失败Activity无效/已销毁");
return; return;
} }
// 先检查权限,已拥有则直接返回
// 已拥有权限,直接返回
if (checkIgnoreBatteryOptimizationPermission(activity)) { if (checkIgnoreBatteryOptimizationPermission(activity)) {
LogUtils.d(TAG, "电池优化权限-请求】已拥有权限,无需发起申请"); LogUtils.d(TAG, "电池优化权限-申请:已拥有权限,无需发起");
return; return;
} }
LogUtils.w(TAG, "【电池优化权限-请求】未拥有权限,开始发起申请");
try { try {
// 方案1直接跳转权限申请页(用户一键同意,优先使用 // 方案1直接跳转一键授权页(优先使用,用户操作简单
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getPackageName())); intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION); activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.d(TAG, "电池优化权限-请求】跳转系统权限申请页"); LogUtils.d(TAG, "电池优化权限-申请:跳转一键授权");
} catch (Exception e) { } catch (Exception e) {
// 方案2备用跳转(跳转优化管理列表,引导手动选择) // 方案2备用跳转优化管理页+提示
Intent intent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS); Intent intent = new Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS);
activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION); activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_OPTIMIZATION);
LogUtils.w(TAG, "电池优化权限-请求】跳转优化管理页(引导手动开启)"); LogUtils.w(TAG, "电池优化权限-申请:跳转失败,引导手动操作");
showBatteryOptTipsDialog(activity); 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) { private void showAutoStartTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
.setTitle("权限申请提示") .setTitle("自启动权限申请提示")
.setMessage("请手动开启自启动权限,否则应用后台功能可能异常:\n1. 进入安全中心 → 应用管理 → 自启动管理\n2. 找到本应用,开启「允许自启动」开关") .setMessage("请手动开启自启动权限,否则应用后台保活异常:\n1. 进入小米安全中心 → 应用管理 → 自启动管理\n2. 找到本应用,开启「允许自启动」开关")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() { .setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
@@ -198,16 +296,16 @@ public class PermissionUtils {
}) })
.setCancelable(false) .setCancelable(false)
.show(); .show();
LogUtils.d(TAG, "自启动权限显示手动开启提示弹窗"); LogUtils.d(TAG, "自启动权限显示手动开启提示弹窗");
} }
/** /**
* 显示电池优化权限手动开启提示弹窗(跳转失败时使用) * 电池优化权限手动开启提示弹窗
*/ */
private void showBatteryOptTipsDialog(final Activity activity) { private void showBatteryOptTipsDialog(final Activity activity) {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
.setTitle("权限申请提示") .setTitle("电池优化权限申请提示")
.setMessage("请手动忽略电池优化,否则应用后台运行可能被限制:\n1. 进入设置 → 电池 → 电池优化\n2. 找到本应用,选择「不优化」") .setMessage("请手动忽略电池优化,否则应用后台运行被限制:\n1. 进入设置 → 电池 → 电池优化\n2. 找到本应用,选择「不优化」选项")
.setPositiveButton("知道了", new DialogInterface.OnClickListener() { .setPositiveButton("知道了", new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
@@ -216,7 +314,37 @@ public class PermissionUtils {
}) })
.setCancelable(false) .setCancelable(false)
.show(); .show();
LogUtils.d(TAG, "电池优化权限显示手动开启提示弹窗"); 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,10 +1,5 @@
package cc.winboll.studio.powerbell.utils; 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.ContentResolver;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@@ -19,199 +14,468 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; 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 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");
}};
public static final String TAG = "UriUtil"; // ====================== 新增核心方法Uri 转文件后缀 ======================
/** /**
* 获取真实路径 * 【静态公共方法】根据 Uri 获取文件真实后缀优先MIME类型匹配适配所有Uri场景+小米机型)
* * @param context 上下文非空用于获取ContentResolver
* @param context * @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) { public static String getFilePathFromUri(Context context, Uri uri) {
if (uri == null) { LogUtils.d(TAG, "=== getFilePathFromUri 调用 start ===");
if (context == null) {
LogUtils.e(TAG, "getFilePathFromUriContext 为空,转换失败");
return null; return null;
} }
switch (uri.getScheme()) { if (uri == null) {
case ContentResolver.SCHEME_CONTENT: LogUtils.e(TAG, "getFilePathFromUriUri 为空,转换失败");
//Android7.0之后的uri content:// URI return null;
return getFilePathFromContentUri(context, uri);
case ContentResolver.SCHEME_FILE:
default:
//Android7.0之前的uri file://
return new File(uri.getPath()).getAbsolutePath();
} }
}
/** String scheme = uri.getScheme();
* 从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; String filePath = null;
// 按 Uri Scheme 分类处理
if (uri.getAuthority() != null) { if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
try { LogUtils.d(TAG, "getFilePathFromUriScheme=content执行ContentUri转换");
inputStream = context.getContentResolver().openInputStream(uri); filePath = getFilePathFromContentUri(context, uri);
File file = createTemporalFileFrom(context, inputStream, fileName); } else if (ContentResolver.SCHEME_FILE.equals(scheme)) {
filePath = file.getPath(); LogUtils.d(TAG, "getFilePathFromUriScheme=file直接转换路径");
filePath = new File(uri.getPath()).getAbsolutePath();
} catch (Exception e) { } else {
} finally { LogUtils.w(TAG, "getFilePathFromUri未知Scheme=" + scheme + ",转换失败");
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
}
}
} }
LogUtils.d(TAG, "=== getFilePathFromUri 调用 end结果" + filePath + " ===");
return filePath; 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对象的绝对路径和存在性 * 文件路径转 Uri核心方法适配 Android7.0+ FileProviderAPI29-30兼容
LogUtils.d(TAG, "getUriForFile -> 文件绝对路径:" + file.getAbsolutePath()); * @param context 上下文(非空)
LogUtils.d(TAG, "getUriForFile -> 文件是否存在:" + file.exists()); * @param filePath 真实文件路径(非空)
LogUtils.d(TAG, "getUriForFile -> 是否为目录:" + file.isDirectory()); * @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;
}
// 3. 合法性校验 // 2. File 对象初始化与校验
if (!file.exists() || file.isDirectory()) { File file = new File(filePath);
LogUtils.e(TAG, "getUriForFile -> 非法路径:文件不存在或为目录"); LogUtils.d(TAG, "getUriForFile:文件路径=" + file.getAbsolutePath() + ",是否存在=" + file.exists());
return null; if (!file.exists() || file.isDirectory()) {
} LogUtils.e(TAG, "getUriForFile文件不存在或为目录转换失败");
return null;
}
// 4. 校验路径是否在配置的合法目录内 // 3. 合法路径校验适配小米机型避免FileProvider配置外路径
String appFilesDir = context.getExternalFilesDir(null) != null ? context.getExternalFilesDir(null).getAbsolutePath() : "null"; if (!isPathInValidDir(context, file)) {
String publicPicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/PowerBell/"; LogUtils.w(TAG, "getUriForFile路径不在安全配置目录内小米机型可能出现权限异常");
String internalFilesDir = context.getFilesDir().getAbsolutePath(); }
String cacheDir = context.getCacheDir().getAbsolutePath();
String absolutePath = file.getAbsolutePath(); // 4. 调用重载方法生成 Uri
boolean isInConfigDir = absolutePath.startsWith(appFilesDir) Uri uri = getUriForFile(context, file);
|| absolutePath.startsWith(publicPicDir) LogUtils.d(TAG, "=== getUriForFile路径版调用 end结果" + (uri != null ? uri.toString() : "null") + " ===");
|| absolutePath.startsWith(internalFilesDir) return uri;
|| absolutePath.startsWith(cacheDir); }
LogUtils.d(TAG, "getUriForFile -> 路径是否在配置目录内:" + isInConfigDir);
if (!isInConfigDir) {
LogUtils.w(TAG, "getUriForFile -> 路径不在FileProvider配置范围内可能导致异常");
// 非强制拦截,保留原有逻辑,仅警告
}
return getUriForFile(context, file); /**
} * 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;
}
public static Uri getUriForFile(Context context, File file) { // 2. 按系统版本生成 UriAPI24+ 强制 FileProvider适配小米机型
if (context == null) { Uri uri = null;
LogUtils.e(TAG, "getUriForFile -> Context为空"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return null; LogUtils.d(TAG, "getUriForFileAndroid7.0+使用FileProvider生成Uri");
} String authority = context.getPackageName() + FILE_PROVIDER_SUFFIX;
if (file == null) { LogUtils.d(TAG, "getUriForFileFileProvider Authority=" + authority);
LogUtils.e(TAG, "getUriForFile -> File对象为空"); try {
return null; 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());
}
// 1. 二次校验文件状态 LogUtils.d(TAG, "=== getUriForFileFile版调用 end ===");
LogUtils.d(TAG, "getUriForFile(File) -> 文件路径:" + file.getAbsolutePath()); return uri;
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"); * ContentUri 转真实路径(适配 content:// 格式处理小米机型特殊Uri
try { * @param context 上下文
String authority = context.getPackageName() + ".fileprovider"; * @param uri ContentUricontent://media/external/file/xxx
LogUtils.d(TAG, "getUriForFile -> FileProvider authority" + authority); * @return 真实文件路径(失败返回 null
Uri uri = FileProvider.getUriForFile(context, authority, file); */
LogUtils.d(TAG, "getUriForFile -> 生成Content Uri成功:" + uri.toString()); private static String getFilePathFromContentUri(Context context, Uri uri) {
return uri; LogUtils.d(TAG, "getFilePathFromContentUriUri=" + uri.toString());
} catch (IllegalArgumentException e) { String filePath = null;
LogUtils.e(TAG, "getUriForFile -> FileProvider生成Uri失败路径未配置或权限不足", e); Cursor cursor = null;
return null; // Java7 语法try-catch-finally 手动关闭Cursor避免内存泄漏
} try {
} else { // 查询字段:优先 DATA 字段,失败则通过文件名+流拷贝获取
LogUtils.d(TAG, "getUriForFile -> Android 7.0以下使用Uri.fromFile生成Uri"); String[] queryColumns = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
Uri uri = Uri.fromFile(file); cursor = context.getContentResolver().query(uri, queryColumns, null, null, null);
LogUtils.d(TAG, "getUriForFile -> 生成File Uri成功" + uri.toString()); if (cursor != null && cursor.moveToFirst()) {
return uri; // 优先读取 DATA 字段(直接获取路径)
} int dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
} if (dataIndex != -1) {
filePath = cursor.getString(dataIndex);
private static File createTemporalFileFrom(Context context, InputStream inputStream, String fileName) LogUtils.d(TAG, "getFilePathFromContentUri从DATA字段获取路径=" + filePath);
throws IOException { } 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; 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) { if (inputStream != null) {
int read;
byte[] buffer = new byte[8 * 1024]; byte[] buffer = new byte[8 * 1024];
//自己定义拷贝文件路径 int readLength;
targetFile = new File(context.getExternalCacheDir(), fileName); targetFile = new File(context.getExternalCacheDir(), fileName);
if (targetFile.exists()) { if (targetFile.exists()) {
targetFile.delete(); targetFile.delete();
} }
OutputStream outputStream = new FileOutputStream(targetFile); OutputStream outputStream = new FileOutputStream(targetFile);
while ((readLength = inputStream.read(buffer)) != -1) {
while ((read = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, readLength);
outputStream.write(buffer, 0, read);
} }
outputStream.flush(); outputStream.flush();
outputStream.close();
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
} }
return targetFile; 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

@@ -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,135 +13,132 @@
android:gravity="center_vertical" android:gravity="center_vertical"
style="@style/DefaultAToolbar"/> style="@style/DefaultAToolbar"/>
<LinearLayout <RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent" 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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
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 <LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:gravity="right">
<cc.winboll.studio.powerbell.views.BackgroundView <cc.winboll.studio.libaes.views.AButton
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="50dp"
android:orientation="vertical" android:layout_height="36dp"
android:layout_width="match_parent" android:text="◎"
android:layout_height="match_parent" android:layout_gravity="center_vertical"
android:id="@+id/background_view"> 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>
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="400dp"> android:layout_height="wrap_content"
android:gravity="right">
<RelativeLayout <cc.winboll.studio.libaes.views.AButton
android:layout_width="match_parent" android:layout_width="50dp"
android:layout_height="wrap_content"> 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 <cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp" android:layout_width="50dp"
android:layout_height="36dp" android:layout_height="36dp"
android:text="Origin BG" android:text="[+~]"
android:id="@+id/activitybackgroundpictureAButton5" android:layout_gravity="center_vertical"
android:layout_alignParentLeft="true" android:layout_margin="5dp"
android:layout_margin="5dp"/> android:id="@+id/activitybackgroundpictureAButton6"/>
<cc.winboll.studio.libaes.views.AButton <cc.winboll.studio.libaes.views.AButton
android:layout_width="160dp" android:layout_width="50dp"
android:layout_height="36dp" android:layout_height="36dp"
android:text="Received BG" android:text="[◐]"
android:id="@+id/activitybackgroundpictureAButton4" android:layout_gravity="center_vertical"
android:layout_alignParentRight="true" android:layout_margin="5dp"
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 <cc.winboll.studio.libaes.views.AButton
android:orientation="horizontal" android:layout_width="50dp"
android:layout_width="match_parent" android:layout_height="36dp"
android:layout_height="wrap_content" android:text="[○]"
android:gravity="right"> android:layout_gravity="center_vertical"
android:layout_margin="5dp"
<cc.winboll.studio.libaes.views.AButton android:id="@+id/activitybackgroundpictureAButton8"/>
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>
</LinearLayout> </LinearLayout>
</RelativeLayout> </LinearLayout>
</LinearLayout> </RelativeLayout>
</LinearLayout> </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="colorUsege">@color/colorRed</color>
<color name="colorCurrent">@color/colorBlue</color> <color name="colorCurrent">@color/colorBlue</color>
<color name="colorCharge">@color/colorYellow</color> <color name="colorCharge">@color/colorYellow</color>
<!--CustomSlideToUnlockView控件配置--> <!--CustomSlideToUnlockView控件配置-->
<color name="colorCustomSlideToUnlockViewWhite">#FFFFFFFF</color> <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> </resources>

View File

@@ -26,4 +26,18 @@
<item name="android:textSize">@dimen/text_subtitle_size</item> <item name="android:textSize">@dimen/text_subtitle_size</item>
</style> </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> </resources>