diff --git a/powerbell/build.gradle b/powerbell/build.gradle index a4a5b862..5810634a 100644 --- a/powerbell/build.gradle +++ b/powerbell/build.gradle @@ -39,14 +39,7 @@ android { } } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - // 米盟 + // 米盟 SDK packagingOptions { doNotStrip "*/*/libmimo_1011.so" } @@ -55,14 +48,15 @@ android { dependencies { // 米盟 - implementation 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk + api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk //注意:以下5个库必须要引入 - //implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'com.google.code.gson:gson:2.8.5' - implementation 'com.github.bumptech.glide:glide:4.9.0' + //api 'androidx.appcompat:appcompat:1.4.1' + api 'androidx.recyclerview:recyclerview:1.0.0' + api 'com.google.code.gson:gson:2.8.5' + api 'com.github.bumptech.glide:glide:4.9.0' //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' + // 应用介绍页类库 api 'io.github.medyo:android-about-page:2.0.0' // SSH @@ -83,7 +77,7 @@ dependencies { //api 'androidx.vectordrawable:vectordrawable-animated:1.1.0' //api 'androidx.fragment:fragment:1.1.0' - implementation 'cc.winboll.studio:libaes:15.11.4' + implementation 'cc.winboll.studio:libaes:15.11.6' implementation 'cc.winboll.studio:libappbase:15.11.0' //api fileTree(dir: 'libs', include: ['*.aar']) diff --git a/powerbell/build.properties b/powerbell/build.properties index 2fed1875..0d8288f7 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Nov 19 09:09:24 HKT 2025 -stageCount=4 +#Wed Nov 26 16:27:33 HKT 2025 +stageCount=9 libraryProject= baseVersion=15.11 -publishVersion=15.11.3 +publishVersion=15.11.8 buildCount=0 -baseBetaVersion=15.11.4 +baseBetaVersion=15.11.9 diff --git a/powerbell/proguard-rules.pro b/powerbell/proguard-rules.pro index ce666849..855b18ac 100644 --- a/powerbell/proguard-rules.pro +++ b/powerbell/proguard-rules.pro @@ -9,12 +9,135 @@ # Add any project specific keep options here: -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# ============================== 基础通用规则 ============================== +# 保留系统组件 +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference -## 米盟 +# 保留 WinBoLL 核心包及子类(合并简化规则) +-keep class cc.winboll.studio.** { *; } +-keepclassmembers class cc.winboll.studio.** { *; } + +# 保留所有类中的 public static final String TAG 字段(便于日志定位) +-keepclassmembers class * { + public static final java.lang.String TAG; +} + +# 保留序列化类(避免Parcelable/Gson解析异常) +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# 保留 R 文件(避免资源ID混淆) +-keepclassmembers class **.R$* { + public static ; +} + +# 保留 native 方法(避免JNI调用失败) +-keepclasseswithmembernames class * { + native ; +} + +# 保留注解和泛型(避免反射/序列化异常) +-keepattributes *Annotation* +-keepattributes Signature + +# 屏蔽 Java 8+ 警告(适配 Java 7 语法) +-dontwarn java.lang.invoke.* +-dontwarn android.support.v8.renderscript.* +-dontwarn java.util.function.** + +# ============================== 第三方框架专项规则 ============================== +# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容) +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-keep class okhttp3.internal.** { *; } +-keep class okio.** { *; } +-dontwarn okhttp3.internal.platform.** +-dontwarn okio.** +# ============================== 必要补充规则 ============================== +# OkHttp 4.4.1 补充规则(Java 7 兼容) +-keep class okhttp3.internal.concurrent.** { *; } +-keep class okhttp3.internal.connection.** { *; } +-dontwarn okhttp3.internal.concurrent.TaskRunner +-dontwarn okhttp3.internal.connection.RealCall + +# Glide 4.9.0(米盟广告图片加载依赖) +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType { + **[] $VALUES; + public *; +} +-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule { + (); +} +-dontwarn com.bumptech.glide.** + +# Gson 2.8.5(米盟广告数据序列化依赖) +-keep class com.google.gson.** { *; } +-keep interface com.google.gson.** { *; } +-keepclassmembers class * { + @com.google.gson.annotations.SerializedName ; +} + +# 米盟 SDK(核心广告组件,完整保留避免加载失败) -keep class com.miui.zeus.** { *; } +-keep interface com.miui.zeus.** { *; } +# 保留米盟日志字段(便于广告加载失败排查) +-keepclassmembers class com.miui.zeus.mimo.sdk.** { + public static final java.lang.String TAG; +} + +# RecyclerView 1.0.0(米盟广告布局渲染依赖) +-keep class androidx.recyclerview.** { *; } +-keep interface androidx.recyclerview.** { *; } +-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter { + public *; +} + +# 其他第三方框架(按引入依赖保留,无则可删除) +# XXPermissions 18.63 +-keep class com.hjq.permissions.** { *; } +-keep interface com.hjq.permissions.** { *; } + +# ZXing 二维码(核心解析组件) +-keep class com.google.zxing.** { *; } +-keep class com.journeyapps.zxing.** { *; } + +# Jsoup HTML解析 +-keep class org.jsoup.** { *; } + +# Pinyin4j 拼音搜索 +-keep class net.sourceforge.pinyin4j.** { *; } + +# JSch SSH组件 +-keep class com.jcraft.jsch.** { *; } + +# AndroidX 基础组件 +-keep class androidx.appcompat.** { *; } +-keep interface androidx.appcompat.** { *; } + +# ============================== 优化与调试配置 ============================== +# 优化级别(平衡混淆效果与性能) +-optimizationpasses 5 +-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* + +# 调试辅助(保留行号便于崩溃定位) +-verbose +-dontpreverify +-dontusemixedcaseclassnames +-keepattributes SourceFile,LineNumberTable + diff --git a/powerbell/src/beta/AndroidManifest.xml b/powerbell/src/beta/AndroidManifest.xml index 7ca0b757..43d1ccfb 100644 --- a/powerbell/src/beta/AndroidManifest.xml +++ b/powerbell/src/beta/AndroidManifest.xml @@ -6,18 +6,6 @@ tools:replace="android:icon" android:icon="@drawable/ic_launcher_beta"> - - - - - - diff --git a/powerbell/src/beta/res/values-zh/string.xml b/powerbell/src/beta/res/values-zh/string.xml index e52345f7..633b11b7 100644 --- a/powerbell/src/beta/res/values-zh/string.xml +++ b/powerbell/src/beta/res/values-zh/string.xml @@ -1,6 +1,7 @@ - PowerBell☆ + 能源钟★ + 泡额呗额☆ diff --git a/powerbell/src/beta/res/xml/shortcutsmaincn1.xml b/powerbell/src/beta/res/xml/shortcutsmaincn1.xml new file mode 100644 index 00000000..d2297b89 --- /dev/null +++ b/powerbell/src/beta/res/xml/shortcutsmaincn1.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/powerbell/src/beta/res/xml/shortcutsmaincn2.xml b/powerbell/src/beta/res/xml/shortcutsmaincn2.xml new file mode 100644 index 00000000..1bf9db12 --- /dev/null +++ b/powerbell/src/beta/res/xml/shortcutsmaincn2.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/powerbell/src/beta/res/xml/shortcutsmainen1.xml b/powerbell/src/beta/res/xml/shortcutsmainen1.xml new file mode 100644 index 00000000..d56a42ef --- /dev/null +++ b/powerbell/src/beta/res/xml/shortcutsmainen1.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/powerbell/src/main/AndroidManifest.xml b/powerbell/src/main/AndroidManifest.xml index af149d28..dcc07343 100644 --- a/powerbell/src/main/AndroidManifest.xml +++ b/powerbell/src/main/AndroidManifest.xml @@ -1,13 +1,14 @@ - - - - - + + + + + + @@ -36,22 +37,22 @@ - + + - - + - - + - - + + android:label="@string/app_name" + android:exported="true" + android:launchMode="singleTask"> + + + + + + @@ -78,7 +92,55 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java index 6d285616..5bd248ce 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java @@ -13,7 +13,14 @@ import java.io.File; public class App extends GlobalApplication { public static final String TAG = "GlobalApplication"; - + + public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1"; + public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1"; + public static final String COMPONENT_CN2 = "cc.winboll.studio.powerbell.MainActivityCN2"; + public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_EN1"; + public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN1"; + public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.powerbell.App.ACTION_SWITCHTO_CN2"; + // 数据配置存储工具 static AppConfigUtils _mAppConfigUtils; static AppCacheUtils _mAppCacheUtils; diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java index 31994b5e..ea23ddf5 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java @@ -23,6 +23,7 @@ import cc.winboll.studio.powerbell.activities.ClearRecordActivity; import cc.winboll.studio.powerbell.activities.WinBoLLActivity; import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; import cc.winboll.studio.powerbell.fragments.MainViewFragment; +import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity; import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils; /** @@ -37,6 +38,8 @@ public class MainActivity extends WinBoLLActivity { public static final String TAG = "MainActivity"; + private static final int REQUEST_WRITE_STORAGE_PERMISSION = 1001; + // private static final String PRIVACY_FILE = "privacy_pfs"; // private static final String PRIVACY_VALUE = "privacy_value";//0: 拒绝,1:赞同 // @@ -81,7 +84,7 @@ public class MainActivity extends WinBoLLActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mADsBannerView = findViewById(R.id.adsbanner); - + // mContainer = findViewById(R.id.ads_container); // // // 初始化主线程Handler(关键:确保广告操作在主线程执行) @@ -119,7 +122,7 @@ public class MainActivity extends WinBoLLActivity { // if (mMainHandler != null) { // mMainHandler.removeCallbacksAndMessages(null); // } - if(mADsBannerView != null) { + if (mADsBannerView != null) { mADsBannerView.releaseAdResources(); } } @@ -165,7 +168,7 @@ public class MainActivity extends WinBoLLActivity { public static void reloadBackground() { // 修复:添加非空校验,避免Activity已销毁时调用 if (_mMainActivity != null && !_mMainActivity.isFinishing() && !_mMainActivity.isDestroyed()) { - _mMainActivity.mMainViewFragment.loadBackground(); + _mMainActivity.mMainViewFragment.reloadBackground(); } } @@ -194,7 +197,7 @@ public class MainActivity extends WinBoLLActivity { super.onResume(); reloadBackground(); setBackgroundColor(); - if(mADsBannerView != null) { + if (mADsBannerView != null) { mADsBannerView.resumeADs(); } @@ -221,6 +224,9 @@ public class MainActivity extends WinBoLLActivity { public boolean onCreateOptionsMenu(Menu menu) { mMenu = menu; getMenuInflater().inflate(R.menu.toolbar_main, mMenu); + if (App.isDebugging()) { + getMenuInflater().inflate(R.menu.toolbar_unittest, mMenu); + } return true; } @@ -238,6 +244,8 @@ public class MainActivity extends WinBoLLActivity { startActivity(new Intent(this, BackgroundPictureActivity.class)); } else if (menuItemId == R.id.action_log) { LogActivity.startLogActivity(this); + } else if (menuItemId == R.id.action_unittestactivity) { + startActivity(new Intent(this, MainUnitTestActivity.class)); } return true; } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/AboutActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/AboutActivity.java index 63ea7804..9a3e764e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/AboutActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/AboutActivity.java @@ -57,7 +57,7 @@ public class AboutActivity extends Activity { appInfo.setAppGitOwner("Studio"); appInfo.setAppGitAPPBranch(szBranchName); appInfo.setAppGitAPPSubProjectFolder(szBranchName); - appInfo.setAppHomePage("https://discuz.winboll.cc/forum.php?mod=viewthread&tid=1"); + appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=PowerBell"); appInfo.setAppAPKName("PowerBell"); appInfo.setAppAPKFolderName("PowerBell"); return new AboutView(mContext, appInfo); diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundPictureActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundPictureActivity.java index 9bf3d440..05d7d242 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundPictureActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundPictureActivity.java @@ -7,16 +7,16 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.text.TextUtils; import android.view.View; -import android.widget.ImageView; import android.widget.RelativeLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog; import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; @@ -24,9 +24,11 @@ import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.R; import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; +import cc.winboll.studio.powerbell.dialogs.NetworkBackgroundDialog; import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils; import cc.winboll.studio.powerbell.utils.FileUtils; import cc.winboll.studio.powerbell.utils.UriUtil; +import cc.winboll.studio.powerbell.views.BackgroundView; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; @@ -53,6 +55,10 @@ public class BackgroundPictureActivity extends WinBoLLActivity implements Backgr private File mfTempCropPicture; // 剪裁临时文件 private File mfRecivedCropPicture; // 剪裁后的目标文件 + private String preViewFileBackgroundView = ""; + BackgroundView bvPreviewBackground; + boolean isCommitSettings = false; + // 静态变量 public static String _mszRecivedCropPicture = "RecivedCrop.jpg"; private static String _mszCommonFileType = "jpeg"; @@ -102,7 +108,7 @@ public class BackgroundPictureActivity extends WinBoLLActivity implements Backgr mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - finish(); + finish(); // 点击导航栏返回按钮,触发 finish() } }); @@ -157,31 +163,36 @@ public class BackgroundPictureActivity extends WinBoLLActivity implements Backgr */ public void updatePreviewBackground() { LogUtils.d(TAG, "updatePreviewBackground"); - ImageView ivPreviewBackground = (ImageView) findViewById(R.id.activitybackgroundpictureImageView1); + //ImageView ivPreviewBackground = (ImageView) findViewById(R.id.activitybackgroundpictureImageView1); + bvPreviewBackground = (BackgroundView) findViewById(R.id.activitybackgroundpictureBackgroundView1); BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this); utils.loadBackgroundPictureBean(); boolean isUseBackgroundFile = utils.getBackgroundPictureBean().isUseBackgroundFile(); if (isUseBackgroundFile && mfRecivedCropPicture.exists()) { - try { - String filePath = utils.getBackgroundDir() + getBackgroundFileName(); - Drawable drawable = FileUtils.getImageDrawable(filePath); - if (drawable != null) { - //drawable.setAlpha(120); - ivPreviewBackground.setImageDrawable(drawable); - } - //ToastUtils.show("背景图片已更新"); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("背景图片加载失败"); - } + //try { + String filePath = utils.getBackgroundDir() + getBackgroundFileName(); + preViewFileBackgroundView = filePath; + bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView); + /*Drawable drawable = FileUtils.getImageDrawable(filePath); + if (drawable != null) { + //drawable.setAlpha(120); + //bvPreviewBackground.setImageDrawable(drawable); + }*/ + //ToastUtils.show("背景图片已更新"); +// } catch (IOException e) { +// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); +// ToastUtils.show("背景图片加载失败"); +// } } else { ToastUtils.show("未使用背景图片"); - Drawable drawable = getResources().getDrawable(R.drawable.blank10x10); - if (drawable != null) { - drawable.setAlpha(120); - ivPreviewBackground.setImageDrawable(drawable); - } + preViewFileBackgroundView = ""; + bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView); +// Drawable drawable = getResources().getDrawable(R.drawable.blank10x10); +// if (drawable != null) { +// drawable.setAlpha(120); +// bvPreviewBackground.setImageDrawable(drawable); +// } } } @@ -433,13 +444,13 @@ public class BackgroundPictureActivity extends WinBoLLActivity implements Backgr fos.close(); } catch (IOException e) { LogUtils.e(TAG, "流关闭异常" + e); - } - } - if (scaledBitmap != null && !scaledBitmap.isRecycled()) { - scaledBitmap.recycle(); - } - } - } + } + } + if (scaledBitmap != null && !scaledBitmap.isRecycled()) { + scaledBitmap.recycle(); + } + } + } /** * 缩放Bitmap @@ -589,5 +600,60 @@ public class BackgroundPictureActivity extends WinBoLLActivity implements Backgr super.onResume(); setBackgroundColor(); } + + public void onNetworkBackgroundDialog(View view) { + // 在需要显示对话框的地方(如网络状态监听回调中) + NetworkBackgroundDialog dialog = new NetworkBackgroundDialog(this, new NetworkBackgroundDialog.OnDialogClickListener() { + @Override + public void onConfirm() { + ToastUtils.show("onConfirm"); + // 处理确认逻辑(如允许后台网络使用) + LogUtils.d("MainActivity", "用户允许后台网络使用"); + // 执行具体业务:如开启后台网络请求服务 + } + + @Override + public void onCancel() { + ToastUtils.show("onCancel"); + // 处理取消逻辑(如禁止后台网络使用) + LogUtils.d("MainActivity", "用户禁止后台网络使用"); + // 执行具体业务:如关闭后台网络请求 + } + }); + + // 可选:修改对话框标题和内容(适配自定义场景) + dialog.setTitle("网络图片下载对话框"); + dialog.setContent("是否下载地址中的图片资源,作为应用背景图片?"); + + // 显示对话框 + dialog.show(); + + } + + /** + * 重写finish方法,确保所有退出场景都触发Toast + */ + @Override + public void finish() { + if (!isCommitSettings) { + YesNoAlertDialog.show(this, "应用背景更改提示:", "是否应用预览图片?", new YesNoAlertDialog.OnDialogResultListener(){ + + @Override + public void onNo() { + isCommitSettings = true; + finish(); + } + + @Override + public void onYes() { + bvPreviewBackground.saveToBackgroundSources(preViewFileBackgroundView); + isCommitSettings = true; + finish(); + } + }); + } else { + super.finish(); + } + } } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ShortcutActionActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ShortcutActionActivity.java new file mode 100644 index 00000000..c22978d1 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/ShortcutActionActivity.java @@ -0,0 +1,49 @@ +package cc.winboll.studio.powerbell.activities; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.R; +import cc.winboll.studio.powerbell.utils.APPPlusUtils; +import cc.winboll.studio.powerbell.App; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/15 13:45 + * @Describe 应用快捷方式活动类 + */ +public class ShortcutActionActivity extends Activity { + + public static final String TAG = "ShortcutActionActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 处理应用级别的切换请求 + handleSwitchRequest(); + finish(); + } + + /** + * 处理应用图标快捷菜单的请求 + */ + private void handleSwitchRequest() { + Intent intent = getIntent(); + if (intent != null && "switchto_en1".equals(intent.getDataString())) { + APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1); + ToastUtils.show("切换至" + getString(R.string.app_name) + "图标"); + //moveTaskToBack(true); + } + if (intent != null && "switchto_cn1".equals(intent.getDataString())) { + APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1); + ToastUtils.show("切换至" + getString(R.string.app_name_cn1) + "图标"); + //moveTaskToBack(true); + } + if (intent != null && "switchto_cn2".equals(intent.getDataString())) { + APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2); + ToastUtils.show("切换至" + getString(R.string.app_name_cn2) + "图标"); + //moveTaskToBack(true); + } + } +} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/NetworkBackgroundDialog.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/NetworkBackgroundDialog.java new file mode 100644 index 00000000..28bb94c8 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/NetworkBackgroundDialog.java @@ -0,0 +1,291 @@ +package cc.winboll.studio.powerbell.dialogs; + +import android.app.Activity; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Message; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.R; +import cc.winboll.studio.powerbell.utils.PictureUtils; +import cc.winboll.studio.powerbell.views.BackgroundView; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/19 20:11 + * @Describe 网络后台使用提示对话框 + * 继承 AndroidX AlertDialog,绑定自定义布局 dialog_networkbackground.xml + */ +public class NetworkBackgroundDialog extends AlertDialog { + public static final String TAG = "NetworkBackgroundDialog"; + // 消息标识:图片加载成功 + private static final int MSG_IMAGE_LOAD_SUCCESS = 1001; + // 消息标识:图片加载失败 + private static final int MSG_IMAGE_LOAD_FAILED = 1002; + + // 控件引用 + private TextView tvTitle; + private TextView tvContent; + private Button btnCancel; + private Button btnConfirm; + private Button btnPreview; + private EditText etURL; + BackgroundView bvBackgroundPreview; + Context mContext; + // 主线程 Handler,用于接收子线程消息并更新 UI + private Handler mUiHandler; + String previewFilePath; + + // 按钮点击回调接口(Java7 接口实现) + public interface OnDialogClickListener { + void onConfirm(); // 确认按钮点击 + void onCancel(); // 取消按钮点击 + } + + private OnDialogClickListener listener; + + // Java7 显式构造(必须传入 Context) + public NetworkBackgroundDialog(@NonNull Context context) { + super(context); + initHandler(); // 初始化 Handler + initView(); // 初始化布局和控件 + setDismissListener(); // 设置对话框消失监听 + } + + // 带回调的构造(便于外部处理点击事件) + public NetworkBackgroundDialog(@NonNull Context context, OnDialogClickListener listener) { + super(context); + this.listener = listener; + initHandler(); // 初始化 Handler + initView(); + setDismissListener(); // 设置对话框消失监听 + } + + /** + * 初始化主线程 Handler,用于更新 UI + */ + private void initHandler() { + mUiHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + // 对话框已消失时,不再处理 UI 消息 + if (!isShowing()) { + return; + } + switch (msg.what) { + case MSG_IMAGE_LOAD_SUCCESS: + // 图片加载成功,获取文件路径并设置背景 + String filePath = (String) msg.obj; + setBackgroundFromPath(filePath); + break; + case MSG_IMAGE_LOAD_FAILED: + // 图片加载失败,设置默认背景 + bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); + ToastUtils.show("图片预览失败,请检查链接"); + break; + } + } + }; + } + + /** + * 设置对话框消失监听:移除 Handler 消息,避免内存泄漏 + */ + private void setDismissListener() { + this.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + // 对话框消失时,移除所有未处理的消息和回调 + if (mUiHandler != null) { + mUiHandler.removeCallbacksAndMessages(null); + } + LogUtils.d(TAG, "对话框已消失,Handler 消息已清理"); + } + }); + } + + /** + * 初始化布局和控件 + */ + private void initView() { + mContext = this.getContext(); + // 加载自定义布局 + View dialogView = LayoutInflater.from(getContext()) + .inflate(R.layout.dialog_networkbackground, null); + // 设置对话框内容视图 + setView(dialogView); + + // 绑定控件 + tvTitle = (TextView) dialogView.findViewById(R.id.tv_dialog_title); + tvContent = (TextView) dialogView.findViewById(R.id.tv_dialog_content); + btnCancel = (Button) dialogView.findViewById(R.id.btn_cancel); + btnConfirm = (Button) dialogView.findViewById(R.id.btn_confirm); + btnPreview = (Button) dialogView.findViewById(R.id.btn_preview); + etURL = (EditText) dialogView.findViewById(R.id.et_url); + bvBackgroundPreview = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview); + + // 设置按钮点击事件 + setButtonClickListeners(); + } + + /** + * 设置按钮点击监听 + */ + private void setButtonClickListeners() { + // 取消按钮:关闭对话框 + 回调外部 + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d("NetworkBackgroundDialog", "取消按钮点击"); + dismiss(); // 关闭对话框 + if (listener != null) { + listener.onCancel(); + } + } + }); + + // 确认按钮:关闭对话框 + 回调外部 + btnConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d("NetworkBackgroundDialog", "确认按钮点击"); + // 确定预览背景资源 + bvBackgroundPreview.saveToBackgroundSources(previewFilePath); + + dismiss(); // 关闭对话框 + if (listener != null) { + listener.onConfirm(); + } + } + }); + + // 图片预览按钮:预览输入框地址图片 + btnPreview.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d("NetworkBackgroundDialog", "确认预览点击"); + downloadImageToAlbumAndPreview(); + /*String url = etURL.getText().toString().trim(); + if (url.isEmpty()) { + ToastUtils.show("请输入图片链接"); + return; + } + ImageDownloader.getInstance(mContext).downloadImage(url, mDownloadCallback);*/ + } + }); + } + + /** + * 根据文件路径设置 BackgroundView 背景(主线程调用) + * @param filePath 图片文件路径 + */ + private void setBackgroundFromPath(String filePath) { + FileInputStream fis = null; + try { + File imageFile = new File(filePath); + if (!imageFile.exists()) { + LogUtils.e(TAG, "图片文件不存在:" + filePath); + ToastUtils.show("Test"); + //bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); + return; + } + + // 预览背景 + previewFilePath = filePath; + bvBackgroundPreview.previewBackgroundImage(previewFilePath); + + LogUtils.d(TAG, "图片预览成功:" + filePath); + + } catch (Exception e) { + e.printStackTrace(); + bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); + LogUtils.e(TAG, "图片预览失败:" + e.getMessage()); + } finally { + // Java7 手动关闭流,避免资源泄漏 + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * 对外提供方法:修改对话框标题(灵活适配不同场景) + */ + public void setTitle(String title) { + if (tvTitle != null) { + tvTitle.setText(title); + } + } + + /** + * 对外提供方法:修改对话框内容(灵活适配不同场景) + */ + public void setContent(String content) { + if (tvContent != null) { + tvContent.setText(content); + } + } + + /** + * 对外提供方法:设置按钮点击回调(替代带参构造) + */ + public void setOnDialogClickListener(OnDialogClickListener listener) { + this.listener = listener; + } + + /*ImageDownloader.DownloadCallback mDownloadCallback = new ImageDownloader.DownloadCallback() { + @Override + public void onSuccess(String filePath) { + ToastUtils.show("图片下载成功:" + filePath); + LogUtils.d(TAG, filePath); + // 发送消息到主线程,携带图片路径 + Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, filePath); + mUiHandler.sendMessage(successMsg); + } + + @Override + public void onFailure(String errorMsg) { + ToastUtils.show("下载失败:" + errorMsg); + LogUtils.e(TAG, errorMsg); + // 发送图片加载失败消息 + mUiHandler.sendEmptyMessage(MSG_IMAGE_LOAD_FAILED); + } + };*/ + + void downloadImageToAlbumAndPreview() { + //String imgUrl = "https://example.com/test.jpg"; + String imgUrl = etURL.getText().toString(); + PictureUtils.downloadImageToAlbum(mContext, imgUrl, new PictureUtils.DownloadCallback(){ + @Override + public void onSuccess(String savePath) { + ToastUtils.show("下载成功:" + savePath); + // 发送消息到主线程,携带图片路径 + Message successMsg = mUiHandler.obtainMessage(MSG_IMAGE_LOAD_SUCCESS, savePath); + mUiHandler.sendMessage(successMsg); + } + + @Override + public void onFailure(Exception e) { + ToastUtils.show("下载失败:" + e.getMessage()); + } + }); + + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/fragments/MainViewFragment.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/fragments/MainViewFragment.java index 6819a656..10c6ae4b 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/fragments/MainViewFragment.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/fragments/MainViewFragment.java @@ -9,7 +9,6 @@ import android.os.Message; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; @@ -20,15 +19,12 @@ import android.widget.TextView; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.powerbell.App; import cc.winboll.studio.powerbell.R; -import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity; -import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.utils.AppConfigUtils; -import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils; import cc.winboll.studio.powerbell.utils.ServiceUtils; +import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BatteryDrawable; import cc.winboll.studio.powerbell.views.VerticalSeekBar; -import java.io.File; public class MainViewFragment extends Fragment { @@ -72,6 +68,7 @@ public class MainViewFragment extends Fragment { TextView mtvUsegeReminderValue; CheckBox mcbUsegeReminderValue; TextView mtvCurrentValue; + BackgroundView bvPreviewBackground; @Override @@ -79,27 +76,28 @@ public class MainViewFragment extends Fragment { mView = inflater.inflate(R.layout.fragment_mainview, container, false); _mMainViewFragment = MainViewFragment.this; mAppConfigUtils = App.getAppConfigUtils(getActivity()); - + // 获取指定ID的View实例 - final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1); + bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1); + /*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1); - // 注册OnGlobalLayoutListener - mainImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // 获取宽度和高度 - int width = mainImageView.getMeasuredWidth(); - int height = mainImageView.getMeasuredHeight(); + // 注册OnGlobalLayoutListener + mainImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + // 获取宽度和高度 + int width = mainImageView.getMeasuredWidth(); + int height = mainImageView.getMeasuredHeight(); - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(getActivity()); - BackgroundPictureBean bean = utils.loadBackgroundPictureBean(); - bean.setBackgroundWidth(width); - bean.setBackgroundHeight(height); - utils.saveData(); - // 移除监听器以避免内存泄漏 - mainImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); + BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(getActivity()); + BackgroundPictureBean bean = utils.loadBackgroundPictureBean(); + bean.setBackgroundWidth(width); + bean.setBackgroundHeight(height); + utils.saveData(); + // 移除监听器以避免内存泄漏 + mainImageView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + });*/ mDrawableFrame = getActivity().getDrawable(R.drawable.bg_frame); mllLeftSeekBar = (LinearLayout) mView.findViewById(R.id.fragmentmainviewLinearLayout1); @@ -302,22 +300,23 @@ public class MainViewFragment extends Fragment { } } - public void loadBackground() { - BackgroundPictureBean bean = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundPictureBean(); - ImageView imageView = mView.findViewById(R.id.fragmentmainviewImageView1); - String szBackgroundFilePath = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundDir() + BackgroundPictureActivity.getBackgroundFileName(); - File fBackgroundFilePath = new File(szBackgroundFilePath); - LogUtils.d(TAG, "szBackgroundFilePath : " + szBackgroundFilePath); - LogUtils.d(TAG, String.format("fBackgroundFilePath.exists() %s", fBackgroundFilePath.exists())); - if (bean.isUseBackgroundFile() && fBackgroundFilePath.exists()) { - Drawable drawableBackground = Drawable.createFromPath(szBackgroundFilePath); - //drawableBackground.setAlpha(120); - imageView.setImageDrawable(drawableBackground); - } else { - Drawable drawableBackground = getActivity().getDrawable(R.drawable.blank10x10); - //drawableBackground.setAlpha(120); - imageView.setImageDrawable(drawableBackground); - } + public void reloadBackground() { + bvPreviewBackground.reloadBackgroundImage(); +// BackgroundPictureBean bean = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundPictureBean(); +// ImageView imageView = mView.findViewById(R.id.fragmentmainviewImageView1); +// String szBackgroundFilePath = BackgroundPictureUtils.getInstance(getActivity()).getBackgroundDir() + BackgroundPictureActivity.getBackgroundFileName(); +// File fBackgroundFilePath = new File(szBackgroundFilePath); +// LogUtils.d(TAG, "szBackgroundFilePath : " + szBackgroundFilePath); +// LogUtils.d(TAG, String.format("fBackgroundFilePath.exists() %s", fBackgroundFilePath.exists())); +// if (bean.isUseBackgroundFile() && fBackgroundFilePath.exists()) { +// Drawable drawableBackground = Drawable.createFromPath(szBackgroundFilePath); +// //drawableBackground.setAlpha(120); +// imageView.setImageDrawable(drawableBackground); +// } else { +// Drawable drawableBackground = getActivity().getDrawable(R.drawable.blank10x10); +// //drawableBackground.setAlpha(120); +// imageView.setImageDrawable(drawableBackground); +// } } Handler mHandler = new Handler(){ diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java new file mode 100644 index 00000000..10f820f1 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java @@ -0,0 +1,48 @@ +package cc.winboll.studio.powerbell.unittest; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.fragment.app.Fragment; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.R; +import android.widget.Button; +import cc.winboll.studio.powerbell.MainActivity; +import android.content.Intent; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/19 18:16 + * @Describe BackgroundViewTestFragment + */ +public class BackgroundViewTestFragment extends Fragment { + + public static final String TAG = "BackgroundViewTestFragment"; + + View mainView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + //super.onCreateView(inflater, container, savedInstanceState); + + // 非调试状态就结束本线程 + if (!GlobalApplication.isDebugging()) { + Thread.currentThread().destroy(); + } + + mainView = inflater.inflate(R.layout.fragment_test_backgroundview, container, false); + + ((Button)mainView.findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){ + + @Override + public void onClick(View v) { + getActivity().startActivity(new Intent(getActivity(), MainActivity.class)); + } + }); + + ToastUtils.show(String.format("%s onCreate", TAG)); + return mainView; + } +} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java new file mode 100644 index 00000000..a5c91248 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java @@ -0,0 +1,39 @@ +package cc.winboll.studio.powerbell.unittest; + +import android.os.Bundle; +import android.widget.FrameLayout; +import androidx.appcompat.app.AppCompatActivity; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.powerbell.R; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import android.nfc.tech.TagTechnology; +import cc.winboll.studio.libappbase.ToastUtils; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/19 18:04 + * @Describe 单元测试启动主页窗口 + */ +public class MainUnitTestActivity extends AppCompatActivity { + + public static final String TAG = "MainUnitTestActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 非调试状态就退出 + if (!GlobalApplication.isDebugging()) { + finish(); + } + setContentView(R.layout.activity_mainunittest); + + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG); + fragmentTransaction.commit(); + + ToastUtils.show(String.format("%s onCreate", TAG)); + } + +} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/APPPlusUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/APPPlusUtils.java new file mode 100644 index 00000000..162c5ea0 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/APPPlusUtils.java @@ -0,0 +1,164 @@ +package cc.winboll.studio.powerbell.utils; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/26 15:54 + * @Describe 应用图标切换工具类(启用组件时创建对应快捷方式) + */ +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.widget.Toast; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.powerbell.App; +import cc.winboll.studio.powerbell.MainActivity; +import cc.winboll.studio.powerbell.R; + +public class APPPlusUtils { + public static final String TAG = "APPPlusUtils"; + + // 快捷方式配置(名称+图标,需与实际资源匹配) +// private static final String PLUS_SHORTCUT_NAME = "位置服务-Laojun"; +// private static final int PLUS_SHORTCUT_ICON = R.mipmap.ic_launcher; // Laojun 图标资源 + + /** + * 添加Plus组件与图标 + */ + public static boolean switchAppLauncherToComponent(Context context, String componentName) { + if (context == null) { + LogUtils.d(TAG, "切换失败:上下文为空"); + Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败", Toast.LENGTH_SHORT).show(); + return false; + } + + PackageManager pm = context.getPackageManager(); + + ComponentName plusComponentSwitchTo = new ComponentName(context, componentName); + ComponentName plusComponentEN1 = new ComponentName(context, App.COMPONENT_EN1); + ComponentName plusComponentCN1 = new ComponentName(context, App.COMPONENT_CN1); + ComponentName plusComponentCN2 = new ComponentName(context, App.COMPONENT_CN2); + + try { + disableComponent(pm, plusComponentEN1); + disableComponent(pm, plusComponentCN1); + disableComponent(pm, plusComponentCN2); + enableComponent(pm, plusComponentSwitchTo); + + return true; + + } catch (Exception e) { + LogUtils.e(TAG, "图标切换失败:" + e.getMessage()); + Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show(); + return false; + } + } + + /** + * 创建指定组件的桌面快捷方式(自动去重,兼容 Android 8.0+) + * @param component 目标组件(如 LAOJUN_ACTIVITY) + * @param name 快捷方式名称 + * @param iconRes 快捷方式图标资源ID + * @return 是否创建成功 + */ + private static boolean createComponentShortcut(Context context, ComponentName component, String name, int iconRes) { + if (context == null || component == null || name == null || iconRes == 0) { + LogUtils.d(TAG, "快捷方式创建失败:参数为空"); + return false; + } + + // Android 8.0+(API 26+):使用 ShortcutManager(系统推荐) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + PackageManager pm = context.getPackageManager(); + android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class); + if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) { + LogUtils.d(TAG, "系统不支持创建快捷方式"); + return false; + } + + // 检查是否已存在该组件的快捷方式(去重) + for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) { + if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) { + LogUtils.d(TAG, "快捷方式已存在:" + component.getClassName()); + return true; + } + } + + // 构建启动目标组件的意图 + Intent launchIntent = new Intent(Intent.ACTION_MAIN) + .setComponent(component) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // 构建快捷方式信息 + android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName()) + .setShortLabel(name) + .setLongLabel(name) + .setIcon(android.graphics.drawable.Icon.createWithResource(context, iconRes)) + .setIntent(launchIntent) + .build(); + + // 请求创建快捷方式(需用户确认) + shortcutManager.requestPinShortcut(shortcutInfo, null); + return true; + + } catch (Exception e) { + LogUtils.d(TAG, "Android O+ 快捷方式创建失败:" + e.getMessage()); + return false; + } + } else { + // Android 8.0 以下:使用广播(兼容旧机型) + try { + // 构建启动目标组件的意图 + Intent launchIntent = new Intent(Intent.ACTION_MAIN) + .setComponent(component) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + // 构建创建快捷方式的广播意图 + Intent installIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); + installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent); + installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); + installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(context, iconRes)); + installIntent.putExtra("duplicate", false); // 禁止重复创建 + + context.sendBroadcast(installIntent); + return true; + + } catch (Exception e) { + LogUtils.d(TAG, "Android O- 快捷方式创建失败:" + e.getMessage()); + return false; + } + } + } + + /** + * 启用组件(带状态检查,避免重复操作) + */ + private static void enableComponent(PackageManager pm, ComponentName component) { + if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + pm.setComponentEnabledSetting( + component, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS + ); + } + } + + /** + * 禁用组件(带状态检查,避免重复操作) + */ + private static void disableComponent(PackageManager pm, ComponentName component) { + if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { + pm.setComponentEnabledSetting( + component, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS + ); + } + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java new file mode 100644 index 00000000..2e1641de --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageDownloader.java @@ -0,0 +1,294 @@ +package cc.winboll.studio.powerbell.utils; + +import android.content.Context; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; +import cc.winboll.studio.libappbase.LogUtils; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/19 20:52 + * @Describe 图片下载工具类(单例模式) + * 功能:下载网络图片到缓存目录、清理过期文件、获取最新下载文件 + */ +public class ImageDownloader { + public static final String TAG = "ImageDownloader"; + // 单例实例 + private static ImageDownloader sInstance; + // OkHttp 客户端(全局复用,提升性能) + private OkHttpClient mOkHttpClient; + // 缓存目录:/data/data/应用包名/cache/networkdownload + private File mCacheDir; + // 过期时间:7天(单位:毫秒),可按需调整 + private static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000; + + /** + * 私有构造(单例模式禁止外部实例化) + * @param context 上下文(用于获取缓存目录) + */ + private ImageDownloader(Context context) { + // 初始化 OkHttp 客户端(设置超时时间) + mOkHttpClient = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .build(); + + // 初始化缓存目录:networkdownload + initCacheDir(context); + // 初始化时清理过期文件 + clearExpiredFiles(); + } + + /** + * 单例获取方法(线程安全) + * @param context 上下文(建议使用 Application 上下文避免内存泄漏) + * @return 单例实例 + */ + public static synchronized ImageDownloader getInstance(Context context) { + if (sInstance == null) { + // 使用 Application 上下文,防止 Activity 销毁导致的内存泄漏 + sInstance = new ImageDownloader(context.getApplicationContext()); + } + return sInstance; + } + + /** + * 初始化缓存目录:若不存在则创建 + */ + private void initCacheDir(Context context) { + // 获取应用内置缓存目录(无需权限) + File cacheRoot = context.getCacheDir(); + mCacheDir = new File(cacheRoot, "networkdownload"); + + // 若目录不存在则创建(包括父目录) + if (!mCacheDir.exists()) { + boolean isCreated = mCacheDir.mkdirs(); + if (isCreated) { + LogUtils.d("ImageDownloader", "networkdownload 缓存目录创建成功:" + mCacheDir.getAbsolutePath()); + } else { + LogUtils.e("ImageDownloader", "networkdownload 缓存目录创建失败"); + } + } else { + LogUtils.d("ImageDownloader", "networkdownload 缓存目录已存在:" + mCacheDir.getAbsolutePath()); + } + } + + /** + * 清理过期文件(最后修改时间超过 EXPIRE_TIME 的文件) + */ + private void clearExpiredFiles() { + if (mCacheDir == null || !mCacheDir.exists()) { + return; + } + + File[] files = mCacheDir.listFiles(); + if (files == null || files.length == 0) { + LogUtils.d("ImageDownloader", "缓存目录无文件,无需清理"); + return; + } + + long currentTime = System.currentTimeMillis(); + int deleteCount = 0; + + // 遍历所有文件,删除过期文件 + for (File file : files) { + long lastModifyTime = file.lastModified(); + if (currentTime - lastModifyTime > EXPIRE_TIME) { + if (file.delete()) { + deleteCount++; + LogUtils.d("ImageDownloader", "删除过期文件:" + file.getName()); + } else { + LogUtils.e("ImageDownloader", "删除过期文件失败:" + file.getName()); + } + } + } + + LogUtils.d("ImageDownloader", "过期文件清理完成,共删除 " + deleteCount + " 个文件"); + } + + /** + * 下载网络图片到缓存目录 + * @param imageUrl 图片网络链接 + * @param callback 下载结果回调(成功/失败) + */ + public void downloadImage(final String imageUrl, final DownloadCallback callback) { + // 校验参数 + if (TextUtils.isEmpty(imageUrl)) { + if (callback != null) { + callback.onFailure("图片链接为空"); + } + return; + } + + if (mCacheDir == null || !mCacheDir.exists()) { + if (callback != null) { + callback.onFailure("缓存目录不存在"); + } + return; + } + + // 构建 OkHttp 请求 + Request request = new Request.Builder() + .url(imageUrl) + .build(); + + // 异步下载(避免阻塞主线程) + mOkHttpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + // 下载失败,回调主线程 + if (callback != null) { + callback.onFailure("下载失败:" + e.getMessage()); + } + LogUtils.e("ImageDownloader", "图片下载失败:" + e.getMessage()); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) { + // 响应失败(如 404、500) + if (callback != null) { + callback.onFailure("响应失败:" + response.code()); + } + LogUtils.e("ImageDownloader", "图片响应失败,状态码:" + response.code()); + return; + } + + // 响应成功,写入文件 + InputStream inputStream = null; + FileOutputStream outputStream = null; + try { + inputStream = response.body().byteStream(); + // 生成 UUID 唯一文件名(保留原文件后缀) + String fileExtension = getFileExtension(imageUrl); + String fileName = UUID.randomUUID().toString() + fileExtension; + File imageFile = new File(mCacheDir, fileName); + + // 写入文件 + outputStream = new FileOutputStream(imageFile); + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + outputStream.flush(); + + // 下载成功,回调主线程并返回文件路径 + if (callback != null) { + callback.onSuccess(imageFile.getAbsolutePath()); + } + LogUtils.d("ImageDownloader", "图片下载成功:" + imageFile.getAbsolutePath()); + + } catch (IOException e) { + if (callback != null) { + callback.onFailure("文件写入失败:" + e.getMessage()); + } + LogUtils.e("ImageDownloader", "图片写入失败:" + e.getMessage()); + } finally { + // 关闭流(Java7 手动关闭,避免资源泄漏) + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + // 关闭响应体 + if (response.body() != null) { + response.body().close(); + } + } + } + }); + } + + /** + * 获取 networkdownload 目录中最后下载的文件(按修改时间排序) + * @return 最后下载的文件路径(null 表示无文件) + */ + public String getLastDownloadedFile() { + if (mCacheDir == null || !mCacheDir.exists()) { + LogUtils.e("ImageDownloader", "缓存目录不存在"); + return null; + } + + File[] files = mCacheDir.listFiles(); + if (files == null || files.length == 0) { + LogUtils.d("ImageDownloader", "缓存目录无文件"); + return null; + } + + // 按最后修改时间降序排序,取第一个即为最新文件 + File lastFile = files[0]; + for (File file : files) { + if (file.lastModified() > lastFile.lastModified()) { + lastFile = file; + } + } + + LogUtils.d("ImageDownloader", "最后下载的文件:" + lastFile.getAbsolutePath()); + return lastFile.getAbsolutePath(); + } + + /** + * 工具方法:从图片链接中提取文件后缀(如 .png、.jpg) + * @param imageUrl 图片链接 + * @return 文件后缀(含点号,若无法提取则返回 .jpg) + */ + private String getFileExtension(String imageUrl) { + if (TextUtils.isEmpty(imageUrl)) { + return ".jpg"; + } + + int lastDotIndex = imageUrl.lastIndexOf("."); + int lastSlashIndex = imageUrl.lastIndexOf("/"); + // 确保后缀在最后一个斜杠之后,且长度合理(1-5 个字符) + if (lastDotIndex > lastSlashIndex && lastDotIndex < imageUrl.length() - 1) { + String extension = imageUrl.substring(lastDotIndex); + if (extension.length() <= 5) { + return extension.toLowerCase(); // 统一转为小写 + } + } + + // 无法提取后缀时,默认使用 .jpg + return ".jpg"; + } + + /** + * 下载结果回调接口(Java7 接口实现) + */ + public interface DownloadCallback { + /** + * 下载成功 + * @param filePath 图片保存路径 + */ + void onSuccess(String filePath); + + /** + * 下载失败 + * @param errorMsg 失败原因 + */ + void onFailure(String errorMsg); + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PictureUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PictureUtils.java new file mode 100644 index 00000000..17e0c3df --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PictureUtils.java @@ -0,0 +1,207 @@ +package cc.winboll.studio.powerbell.utils; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/21 18:55 + * @Describe + * 图片下载工具类(指定目录保存:Pictures/PowerBell/BackgroundHistory) + */ +public class PictureUtils { + private static final String TAG = "PictureUtils"; + private static final String ROOT_DIR = "PowerBell/BackgroundHistory"; // 自定义目录结构 + private static OkHttpClient sOkHttpClient; + + static { + sOkHttpClient = new OkHttpClient(); + } + + /** + * 下载网络图片到指定目录(外部存储/Pictures/PowerBell/BackgroundHistory) + * @param context 上下文(用于通知相册刷新) + * @param imgUrl 图片网络URL + * @param callback 下载结果回调(成功/失败) + */ + public static void downloadImageToAlbum(final Context context, final String imgUrl, final DownloadCallback callback) { + // 检查参数合法性 + if (context == null) { + if (callback != null) { + callback.onFailure(new IllegalArgumentException("Context不能为空")); + } + return; + } + if (imgUrl == null || imgUrl.isEmpty()) { + if (callback != null) { + callback.onFailure(new IllegalArgumentException("图片URL为空")); + } + return; + } + + startDownload(context, imgUrl, callback); + } + + /** + * 执行实际的下载逻辑 + */ + private static void startDownload(final Context context, final String imgUrl, final DownloadCallback callback) { + Request request = new Request.Builder().url(imgUrl).build(); + sOkHttpClient.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) { + if (callback != null) { + callback.onFailure(new IOException("请求失败,响应码:" + response.code())); + } + return; + } + + InputStream inputStream = null; + FileOutputStream outputStream = null; + try { + inputStream = response.body().byteStream(); + // 1. 获取并创建指定保存目录(外部存储/Pictures/PowerBell/BackgroundHistory) + File saveDir = getTargetSaveDir(context); + if (!saveDir.exists()) { + boolean isDirCreated = saveDir.mkdirs(); // 递归创建多级目录 + if (!isDirCreated) { + if (callback != null) { + callback.onFailure(new IOException("创建目录失败:" + saveDir.getAbsolutePath())); + } + return; + } + } + + // 2. 解析图片后缀 + String fileSuffix = getImageSuffix(imgUrl, response); + // 3. 生成时间戳文件名 + String fileName = generateTimeFileName() + fileSuffix; + // 4. 创建文件 + final File saveFile = new File(saveDir, fileName); + + // 5. 写入文件 + outputStream = new FileOutputStream(saveFile); + byte[] buffer = new byte[1024 * 4]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + outputStream.flush(); + + // 6. 通知相册刷新(使图片显示在系统相册中) + notifyAlbumRefresh(context, saveFile); + + // 成功回调 + if (callback != null) { + callback.onSuccess(saveFile.getAbsolutePath()); + } + + } catch (Exception e) { + Log.e(TAG, "下载图片异常", e); + if (callback != null) { + callback.onFailure(e); + } + } finally { + // 关闭资源 + if (inputStream != null) inputStream.close(); + if (outputStream != null) outputStream.close(); + if (response.body() != null) response.body().close(); + } + } + + @Override + public void onFailure(Call call, final IOException e) { + Log.e(TAG, "下载图片失败", e); + if (callback != null) { + callback.onFailure(e); + } + } + }); + } + + /** + * 获取目标保存目录:外部存储/Pictures/PowerBell/BackgroundHistory + */ + private static File getTargetSaveDir(Context context) { + // 优先使用公共Pictures目录(兼容多数设备) + File publicPicturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + if (publicPicturesDir.exists()) { + return new File(publicPicturesDir, ROOT_DIR); + } + // 备选:应用私有Pictures目录(若公共目录不可用) + File appPicturesDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (appPicturesDir != null) { + return new File(appPicturesDir, ROOT_DIR); + } + // 极端情况:外部存储根目录 + return new File(Environment.getExternalStorageDirectory(), ROOT_DIR); + } + + /** + * 解析图片后缀名 + */ + private static String getImageSuffix(String imgUrl, Response response) { + // 优先从URL解析 + if (imgUrl.lastIndexOf('.') != -1) { + String suffix = imgUrl.substring(imgUrl.lastIndexOf('.')); + if (suffix.length() <= 5 && (suffix.contains("png") || suffix.contains("jpg") || suffix.contains("jpeg") || suffix.contains("gif"))) { + return suffix.toLowerCase(Locale.getDefault()); + } + } + // 从响应头解析 + String contentType = response.header("Content-Type"); + if (contentType != null) { + if (contentType.contains("png")) return ".png"; + if (contentType.contains("jpeg") || contentType.contains("jpg")) return ".jpg"; + if (contentType.contains("gif")) return ".gif"; + } + return ".jpg"; // 默认后缀 + } + + /** + * 生成时间戳文件名 + */ + private static String generateTimeFileName() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault()); + return sdf.format(new Date()); + } + + /** + * 通知相册刷新 + */ + private static void notifyAlbumRefresh(Context context, File file) { + try { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + Uri uri = Uri.fromFile(file); + intent.setData(uri); + context.sendBroadcast(intent); + } catch (Exception e) { + Log.e(TAG, "通知相册刷新失败", e); + } + } + + /** + * 下载结果回调接口 + */ + public interface DownloadCallback { + void onSuccess(String savePath); // 下载成功(子线程回调) + void onFailure(Exception e); // 下载失败(子线程回调) + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java new file mode 100644 index 00000000..5a63951c --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -0,0 +1,374 @@ +package cc.winboll.studio.powerbell.views; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.powerbell.R; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/19 18:01 + * @Describe 背景图片视图控件(支持预览临时图片 + 外部刷新) + */ +public class BackgroundView extends RelativeLayout { + + public static final String TAG = "BackgroundView"; + + Context mContext; + private ImageView ivBackground; + + private static String BACKGROUND_IMAGE_FOLDER = "Background"; + private static String BACKGROUND_IMAGE_FILENAME = "current.data"; + private static String BACKGROUND_IMAGE_PREVIEW_FILENAME = "current_preview.data"; + private static String backgroundSourceFilePath; + private float imageAspectRatio = 1.0f; // 默认 1:1 + // 标记当前是否处于预览状态 + private boolean isPreviewMode = false; + + public BackgroundView(Context context) { + super(context); + this.mContext = context; + initView(); + } + + public BackgroundView(Context context, AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + initView(); + } + + public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.mContext = context; + initView(); + } + + public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + this.mContext = context; + initView(); + } + + void initView() { + initBackgroundImageView(); + initBackgroundImagePath(); + loadAndSetImageViewBackground(); + } + + private void initBackgroundImageView() { + ivBackground = new ImageView(mContext); + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + ivBackground.setLayoutParams(layoutParams); + ivBackground.setScaleType(ImageView.ScaleType.FIT_CENTER); + this.addView(ivBackground); + } + + private void initBackgroundImagePath() { + File externalFilesDir = mContext.getExternalFilesDir(null); + if (externalFilesDir == null) { + LogUtils.e(TAG, "外置存储不可用,无法初始化背景图片路径"); + return; + } + + File backgroundDir = new File(externalFilesDir, BACKGROUND_IMAGE_FOLDER); + if (!backgroundDir.exists()) { + backgroundDir.mkdirs(); + } + backgroundSourceFilePath = new File(backgroundDir, BACKGROUND_IMAGE_FILENAME).getAbsolutePath(); + } + + /** + * 拷贝图片文件到背景资源目录(正式背景) + */ + public void saveToBackgroundSources(String srcBackgroundPath) { + initBackgroundImagePath(); + if (backgroundSourceFilePath == null) { + LogUtils.e(TAG, "目标路径初始化失败,无法保存背景图片"); + return; + } + + File srcFile = new File(srcBackgroundPath); + if (!srcFile.exists() || !srcFile.isFile()) { + LogUtils.e(TAG, String.format("源文件不存在或不是文件:%s", srcBackgroundPath)); + return; + } + + File destFile = new File(backgroundSourceFilePath); + File destDir = destFile.getParentFile(); + if (destDir != null && !destDir.exists()) { + boolean isDirCreated = destDir.mkdirs(); + if (!isDirCreated) { + LogUtils.e(TAG, "目标目录创建失败:" + destDir.getAbsolutePath()); + return; + } + } + + FileInputStream fis = null; + FileOutputStream fos = null; + try { + fis = new FileInputStream(srcFile); + fos = new FileOutputStream(destFile); + + byte[] buffer = new byte[4096]; + int len; + while ((len = fis.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + fos.flush(); + + LogUtils.d(TAG, String.format("文件拷贝成功:%s -> %s", srcBackgroundPath, backgroundSourceFilePath)); + // 拷贝成功后,若处于预览模式则退出预览,加载正式背景 + if (isPreviewMode) { + exitPreviewMode(); + } else { + loadAndSetImageViewBackground(); + } + + } catch (Exception e) { + LogUtils.e(TAG, String.format("文件拷贝失败:%s", e.getMessage()), e); + if (destFile.exists()) { + destFile.delete(); + LogUtils.d(TAG, "已删除损坏的目标文件"); + } + } finally { + if (fis != null) { + try { + fis.close(); + } catch (Exception e) { + LogUtils.e(TAG, "输入流关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (Exception e) { + LogUtils.e(TAG, "输出流关闭失败:" + e.getMessage()); + } + } + } + } + + /** + * 【新增公共函数】预览临时图片(不修改正式背景文件) + * @param previewImagePath 临时预览图片的路径 + */ + public void previewBackgroundImage(String previewImagePath) { + if (previewImagePath == null || previewImagePath.isEmpty()) { + LogUtils.e(TAG, "预览图片路径为空"); + return; + } + + File previewFile = new File(previewImagePath); + if (!previewFile.exists() || !previewFile.isFile()) { + LogUtils.e(TAG, "预览图片不存在或不是文件:" + previewImagePath); + return; + } + + // 计算预览图片宽高比 + if (!calculateImageAspectRatio(previewFile)) { + LogUtils.e(TAG, "预览图片尺寸无效,无法预览"); + return; + } + + // 压缩加载预览图片 + Bitmap previewBitmap = decodeBitmapWithCompress(previewFile, 1080, 1920); + if (previewBitmap == null) { + LogUtils.e(TAG, "预览图片加载失败"); + return; + } + + // 设置预览图片到 ImageView + Drawable previewDrawable = new BitmapDrawable(mContext.getResources(), previewBitmap); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + ivBackground.setBackground(previewDrawable); + } else { + ivBackground.setBackgroundDrawable(previewDrawable); + } + + // 调整 ImageView 尺寸以匹配预览图片宽高比 + adjustImageViewSize(); + isPreviewMode = true; + LogUtils.d(TAG, "进入预览模式,预览图片路径:" + previewImagePath); + } + + /** + * 【新增公共函数】退出预览模式,恢复显示正式背景图片 + */ + public void exitPreviewMode() { + if (isPreviewMode) { + loadAndSetImageViewBackground(); + isPreviewMode = false; + LogUtils.d(TAG, "退出预览模式,恢复正式背景"); + } + } + + /** + * 公共函数:供外部类调用,重新加载正式背景图片(刷新显示) + */ + public void reloadBackgroundImage() { + LogUtils.d(TAG, "外部调用重新加载背景图片"); + initBackgroundImagePath(); + loadAndSetImageViewBackground(); + // 若处于预览模式,退出预览 + if (isPreviewMode) { + isPreviewMode = false; + } + } + + /** + * 加载正式背景图片并设置到 ImageView + */ + private void loadAndSetImageViewBackground() { + if (backgroundSourceFilePath == null) { + setDefaultImageViewBackground(); + return; + } + + File backgroundFile = new File(backgroundSourceFilePath); + if (!backgroundFile.exists() || !backgroundFile.isFile()) { + LogUtils.e(TAG, "背景图片不存在:" + backgroundSourceFilePath); + setDefaultImageViewBackground(); + return; + } + + if (!calculateImageAspectRatio(backgroundFile)) { + setDefaultImageViewBackground(); + return; + } + + Bitmap bitmap = decodeBitmapWithCompress(backgroundFile, 1080, 1920); + if (bitmap == null) { + LogUtils.e(TAG, "图片加载失败,无法解析为 Bitmap"); + setDefaultImageViewBackground(); + return; + } + + Drawable backgroundDrawable = new BitmapDrawable(mContext.getResources(), bitmap); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + ivBackground.setBackground(backgroundDrawable); + } else { + ivBackground.setBackgroundDrawable(backgroundDrawable); + } + + adjustImageViewSize(); + LogUtils.d(TAG, "ImageView 背景加载成功,宽高比:" + imageAspectRatio); + } + + /** + * 计算图片宽高比(宽/高) + */ + private boolean calculateImageAspectRatio(File file) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + + int imageWidth = options.outWidth; + int imageHeight = options.outHeight; + + if (imageWidth <= 0 || imageHeight <= 0) { + LogUtils.e(TAG, "图片尺寸无效:宽=" + imageWidth + ", 高=" + imageHeight); + return false; + } + + imageAspectRatio = (float) imageWidth / imageHeight; + return true; + } catch (Exception e) { + LogUtils.e(TAG, "计算图片宽高比失败:" + e.getMessage()); + return false; + } + } + + /** + * 动态调整 ImageView 尺寸以匹配图片宽高比 + */ + private void adjustImageViewSize() { + int parentWidth = getWidth(); + int parentHeight = getHeight(); + + if (parentWidth == 0 || parentHeight == 0) { + post(new Runnable() { + @Override + public void run() { + adjustImageViewSize(); + } + }); + return; + } + + int imageViewWidth, imageViewHeight; + if (imageAspectRatio >= 1.0f) { // 横图 + imageViewWidth = Math.min(parentWidth, (int) (parentHeight * imageAspectRatio)); + imageViewHeight = (int) (imageViewWidth / imageAspectRatio); + } else { // 竖图 + imageViewHeight = Math.min(parentHeight, (int) (parentWidth / imageAspectRatio)); + imageViewWidth = (int) (imageViewHeight * imageAspectRatio); + } + + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) ivBackground.getLayoutParams(); + layoutParams.width = imageViewWidth; + layoutParams.height = imageViewHeight; + ivBackground.setLayoutParams(layoutParams); + } + + /** + * 带压缩的 Bitmap 解码(避免 OOM) + */ + private Bitmap decodeBitmapWithCompress(File file, int maxWidth, int maxHeight) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + + int scaleX = options.outWidth / maxWidth; + int scaleY = options.outHeight / maxHeight; + int inSampleSize = Math.max(scaleX, scaleY); + if (inSampleSize <= 0) { + inSampleSize = 1; + } + + options.inJustDecodeBounds = false; + options.inSampleSize = inSampleSize; + options.inPreferredConfig = Bitmap.Config.RGB_565; + return BitmapFactory.decodeFile(file.getAbsolutePath(), options); + } catch (Exception e) { + LogUtils.e(TAG, "图片压缩加载失败:" + e.getMessage()); + return null; + } + } + + /** + * 设置默认背景(图片加载失败时兜底) + */ + private void setDefaultImageViewBackground() { + ivBackground.setBackgroundResource(R.drawable.default_background); + imageAspectRatio = 1.0f; + adjustImageViewSize(); + LogUtils.d(TAG, "已设置 ImageView 默认背景"); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + adjustImageViewSize(); + } + + /** + * 对外提供:判断当前是否处于预览模式 + */ + public boolean isPreviewMode() { + return isPreviewMode; + } +} + diff --git a/powerbell/src/main/res/drawable/default_background.xml b/powerbell/src/main/res/drawable/default_background.xml new file mode 100644 index 00000000..f021b2ef --- /dev/null +++ b/powerbell/src/main/res/drawable/default_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/powerbell/src/main/res/layout/activity_backgroundpicture.xml b/powerbell/src/main/res/layout/activity_backgroundpicture.xml index d3f329a3..40ae2bea 100644 --- a/powerbell/src/main/res/layout/activity_backgroundpicture.xml +++ b/powerbell/src/main/res/layout/activity_backgroundpicture.xml @@ -21,14 +21,15 @@ android:layout_height="match_parent" android:id="@+id/activitybackgroundpictureRelativeLayout1"/> - - + android:background="#FF7381FF" + android:id="@+id/activitybackgroundpictureBackgroundView1"> - + + + + + + + - + - + + diff --git a/powerbell/src/main/res/layout/activity_mainunittest.xml b/powerbell/src/main/res/layout/activity_mainunittest.xml new file mode 100644 index 00000000..b8e7d012 --- /dev/null +++ b/powerbell/src/main/res/layout/activity_mainunittest.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/powerbell/src/main/res/layout/dialog_networkbackground.xml b/powerbell/src/main/res/layout/dialog_networkbackground.xml new file mode 100644 index 00000000..34e7ea74 --- /dev/null +++ b/powerbell/src/main/res/layout/dialog_networkbackground.xml @@ -0,0 +1,93 @@ + + + + + + + + + +