diff --git a/powerbell/build.gradle b/powerbell/build.gradle index 5810634..7054e3f 100644 --- a/powerbell/build.gradle +++ b/powerbell/build.gradle @@ -33,7 +33,7 @@ android { // versionName 更新后需要手动设置 // .winboll/winbollBuildProps.properties 文件的 stageCount=0 // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" - versionName "15.11" + versionName "15.12" if(true) { versionName = genVersionName("${versionName}") } @@ -56,7 +56,12 @@ dependencies { api 'com.github.bumptech.glide:glide:4.9.0' //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' - + // uCrop 核心依赖(最新稳定版) + implementation 'com.github.yalantis:ucrop:2.2.8' + // 兼容AndroidX(若项目用AndroidX,必须添加) + //implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.exifinterface:exifinterface:1.3.6' + // 应用介绍页类库 api 'io.github.medyo:android-about-page:2.0.0' // SSH @@ -77,8 +82,8 @@ dependencies { //api 'androidx.vectordrawable:vectordrawable-animated:1.1.0' //api 'androidx.fragment:fragment:1.1.0' - implementation 'cc.winboll.studio:libaes:15.11.6' - implementation 'cc.winboll.studio:libappbase:15.11.0' + implementation 'cc.winboll.studio:libaes:15.11.8' + implementation 'cc.winboll.studio:libappbase:15.11.6' //api fileTree(dir: 'libs', include: ['*.aar']) api fileTree(dir: 'libs', include: ['*.jar']) diff --git a/powerbell/build.properties b/powerbell/build.properties index 0d8288f..3c79fb9 100644 --- a/powerbell/build.properties +++ b/powerbell/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Wed Nov 26 16:27:33 HKT 2025 -stageCount=9 +#Thu Dec 04 10:29:58 GMT 2025 +stageCount=1 libraryProject= -baseVersion=15.11 -publishVersion=15.11.8 -buildCount=0 -baseBetaVersion=15.11.9 +baseVersion=15.12 +publishVersion=15.12.0 +buildCount=6 +baseBetaVersion=15.12.1 diff --git a/powerbell/src/main/AndroidManifest.xml b/powerbell/src/main/AndroidManifest.xml index dcc0734..af1dd9a 100644 --- a/powerbell/src/main/AndroidManifest.xml +++ b/powerbell/src/main/AndroidManifest.xml @@ -150,7 +150,7 @@ @@ -230,6 +230,15 @@ + + + + + + - \ 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 5bd248c..93c776e 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/App.java @@ -2,7 +2,6 @@ package cc.winboll.studio.powerbell; import android.content.Context; import android.os.Environment; -import android.view.Gravity; import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.receivers.GlobalApplicationReceiver; @@ -12,7 +11,7 @@ import java.io.File; public class App extends GlobalApplication { - public static final String TAG = "GlobalApplication"; + public static final String TAG = "App"; public static final String COMPONENT_EN1 = "cc.winboll.studio.powerbell.MainActivityEN1"; public static final String COMPONENT_CN1 = "cc.winboll.studio.powerbell.MainActivityCN1"; @@ -34,6 +33,7 @@ public class App extends GlobalApplication { @Override public void onCreate() { super.onCreate(); + //setIsDebugging(false); setIsDebugging(BuildConfig.DEBUG); // 临时文件夹方案1 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 ea23ddf..1be62a0 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/MainActivity.java @@ -16,15 +16,18 @@ import androidx.appcompat.widget.Toolbar; import cc.winboll.studio.libaes.views.ADsBannerView; import cc.winboll.studio.libappbase.LogActivity; import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; import cc.winboll.studio.powerbell.activities.AboutActivity; -import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity; +import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity; import cc.winboll.studio.powerbell.activities.BatteryReportActivity; import cc.winboll.studio.powerbell.activities.ClearRecordActivity; +import cc.winboll.studio.powerbell.activities.SettingsActivity; import cc.winboll.studio.powerbell.activities.WinBoLLActivity; -import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; import cc.winboll.studio.powerbell.fragments.MainViewFragment; +import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.unittest.MainUnitTestActivity; -import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; +import cc.winboll.studio.powerbell.utils.PermissionUtils; /** * 主活动类(修复小米广告SDK空Context崩溃问题) @@ -109,6 +112,8 @@ public class MainActivity extends WinBoLLActivity { tx.commit(); } showFragment(mMainViewFragment); + + PermissionUtils.getInstance().checkAndRequestStoragePermission(this); } @Override @@ -198,7 +203,7 @@ public class MainActivity extends WinBoLLActivity { reloadBackground(); setBackgroundColor(); if (mADsBannerView != null) { - mADsBannerView.resumeADs(); + mADsBannerView.resumeADs(MainActivity.this); } // // 修复:优化广告请求逻辑(添加生命周期判断 + 主线程执行) @@ -234,14 +239,16 @@ public class MainActivity extends WinBoLLActivity { public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item); int menuItemId = item.getItemId(); - if (menuItemId == R.id.action_about) { + if (menuItemId == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); + } else if (menuItemId == R.id.action_about) { startActivity(new Intent(this, AboutActivity.class)); } else if (menuItemId == R.id.action_battery_report) { startActivity(new Intent(this, BatteryReportActivity.class)); } else if (menuItemId == R.id.action_clearrecord) { startActivity(new Intent(this, ClearRecordActivity.class)); } else if (menuItemId == R.id.action_changepicture) { - startActivity(new Intent(this, BackgroundPictureActivity.class)); + startActivity(new Intent(this, BackgroundSettingsActivity.class)); } else if (menuItemId == R.id.action_log) { LogActivity.startLogActivity(this); } else if (menuItemId == R.id.action_unittestactivity) { @@ -278,8 +285,8 @@ public class MainActivity extends WinBoLLActivity { if (isFinishing() || isDestroyed()) { return; } - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this); - BackgroundPictureBean bean = utils.getBackgroundPictureBean(); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(this); + BackgroundBean bean = utils.getCurrentBackgroundBean(); int nPixelColor = bean.getPixelColor(); RelativeLayout mainLayout = findViewById(R.id.activitymainRelativeLayout1); if (mainLayout != null) { 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 deleted file mode 100644 index 05d7d24..0000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundPictureActivity.java +++ /dev/null @@ -1,659 +0,0 @@ -package cc.winboll.studio.powerbell.activities; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -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.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; -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; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -public class BackgroundPictureActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { - - public static final String TAG = "BackgroundPictureActivity"; - public BackgroundPictureUtils mBackgroundPictureUtils; - - // 图片选择请求码 - public static final int REQUEST_SELECT_PICTURE = 0; - public static final int REQUEST_TAKE_PHOTO = 1; - public static final int REQUEST_CROP_IMAGE = 2; - private static final int STORAGE_PERMISSION_REQUEST = 100; - - private AToolbar mAToolbar; - private File mfBackgroundDir; // 背景图片存储文件夹 - private File mfPictureDir; // 拍照与剪裁临时文件夹 - private File mfTakePhoto; // 拍照文件 - private File mfRecivedPicture; // 接收的图片文件 - 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"; - private int mnPictureCompress = 100; - private static String _RecivedPictureFileName; - - @Override - public Activity getActivity() { - return this; - } - - @Override - public String getTag() { - return TAG; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_backgroundpicture); - initEnv(); - - // 初始化工具类和文件夹 - mBackgroundPictureUtils = BackgroundPictureUtils.getInstance(this); - mfBackgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir()); - if (!mfBackgroundDir.exists()) { - mfBackgroundDir.mkdirs(); - } - - mfPictureDir = new File(App.getTempDirPath()); - if (!mfPictureDir.exists()) { - mfPictureDir.mkdirs(); - } - - // 初始化文件对象 - mfTakePhoto = new File(mfPictureDir, "TakePhoto.jpg"); - mfTempCropPicture = new File(mfPictureDir, "TempCrop.jpg"); - - mfRecivedPicture = getRecivedPictureFile(this); - mfRecivedCropPicture = new File(mfBackgroundDir, _mszRecivedCropPicture); - - // 初始化工具栏 - mAToolbar = (AToolbar) findViewById(R.id.toolbar); - setActionBar(mAToolbar); - mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); - getActionBar().setDisplayHomeAsUpEnabled(true); - mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); // 点击导航栏返回按钮,触发 finish() - } - }); - - // 设置按钮点击事件 - findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); - findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener); - findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener); - findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); - findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); - - updatePreviewBackground(); - - // 处理分享的图片 - Intent intent = getIntent(); - String action = intent.getAction(); - String type = intent.getType(); - - if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { - BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this); - dlg.show(); - } - } - - private void initEnv() { - LogUtils.d(TAG, "initEnv()"); - _RecivedPictureFileName = "Recived.data"; - } - - public static String getBackgroundFileName() { - return _mszRecivedCropPicture; - } - - @Override - public void onAcceptRecivedPicture(String szPreRecivedPictureName) { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(this); - utils.getBackgroundPictureBean().setIsUseBackgroundFile(true); - utils.saveData(); - - File sourceFile = new File(utils.getBackgroundDir(), szPreRecivedPictureName); - if (FileUtils.copyFile(sourceFile, mfRecivedPicture)) { - startCropImageActivity(false); - } else { - ToastUtils.show("图片复制失败,请重试"); - } - } - - /** - * 更新背景图片预览 - */ - public void updatePreviewBackground() { - LogUtils.d(TAG, "updatePreviewBackground"); - //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(); - 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("未使用背景图片"); - preViewFileBackgroundView = ""; - bvPreviewBackground.previewBackgroundImage(preViewFileBackgroundView); -// Drawable drawable = getResources().getDrawable(R.drawable.blank10x10); -// if (drawable != null) { -// drawable.setAlpha(120); -// bvPreviewBackground.setImageDrawable(drawable); -// } - } - } - - // 点击事件监听器 - private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this); - BackgroundPictureBean bean = utils.getBackgroundPictureBean(); - bean.setIsUseBackgroundFile(false); - utils.saveData(); - updatePreviewBackground(); - } - }; - - private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (checkAndRequestStoragePermission()) { - Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - startActivityForResult(intent, REQUEST_SELECT_PICTURE); - } - } - }; - - private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - File fCheck = new File(mfBackgroundDir, getBackgroundFileName()); - if (fCheck.exists()) { - startCropImageActivity(false); - } else { - ToastUtils.show("没有可剪裁的图片"); - } - } - }; - - private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - File fCheck = new File(mfBackgroundDir, getBackgroundFileName()); - if (fCheck.exists()) { - startCropImageActivity(true); - } else { - ToastUtils.show("没有可剪裁的图片"); - } - } - }; - - private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - LogUtils.d(TAG, "onTakePhotoClickListener"); - LogUtils.d(TAG, "mfTakePhoto : " + mfTakePhoto.getPath()); - - if (mfTakePhoto.exists()) { - mfTakePhoto.delete(); - } - try { - mfTakePhoto.createNewFile(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("拍照文件创建失败"); - return; - } - - if (checkAndRequestStoragePermission()) { - Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); - } - } - }; - - private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this); - utils.getBackgroundPictureBean().setIsUseBackgroundFile(true); - utils.saveData(); - updatePreviewBackground(); - } - }; - - private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - // 从文件路径启动像素拾取活动 - //String imagePath = "/storage/emulated/0/DCIM/Camera/sample.jpg"; - String imagePath = mfRecivedCropPicture.toString(); - Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class); - intent.putExtra("imagePath", imagePath); - startActivity(intent); - //App.getWinBoLLActivityManager().startWinBoLLActivity(getActivity(), intent, PixelPickerActivity.class); - } - }; - - private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this); - BackgroundPictureBean bean = utils.getBackgroundPictureBean(); - bean.setPixelColor(0); - utils.saveData(); - setBackgroundColor(); - } - }; - - /** - * 压缩图片并保存到接收文件 - */ - void compressQualityToRecivedPicture(Bitmap bitmap) { - OutputStream outStream = null; - try { - mfRecivedPicture = getRecivedPictureFile(this); - if (!mfRecivedPicture.exists()) { - mfRecivedPicture.createNewFile(); - } - - FileOutputStream fos = new FileOutputStream(mfRecivedPicture); - outStream = new BufferedOutputStream(fos); - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outStream); - outStream.flush(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("图片压缩失败"); - } finally { - if (outStream != null) { - try { - outStream.close(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - } - if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); - } - } - } - - /** - * 启动图片裁剪活动 - * @param isCropFree 是否自由裁剪 - */ - public void startCropImageActivity(boolean isCropFree) { - LogUtils.d(TAG, "startCropImageActivity"); - BackgroundPictureBean bean = mBackgroundPictureUtils.loadBackgroundPictureBean(); - mfRecivedPicture = getRecivedPictureFile(this); - Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture); - LogUtils.d(TAG, "uri : " + uri.toString()); - - if (mfTempCropPicture.exists()) { - mfTempCropPicture.delete(); - } - try { - mfTempCropPicture.createNewFile(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - ToastUtils.show("剪裁临时文件创建失败"); - return; - } - - Uri cropOutPutUri = Uri.fromFile(mfTempCropPicture); - LogUtils.d(TAG, "mfTempCropPicture : " + mfTempCropPicture.getPath()); - - Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(uri, "image/" + _mszCommonFileType); - intent.putExtra("crop", "true"); - intent.putExtra("noFaceDetection", true); - - if (!isCropFree) { - intent.putExtra("aspectX", bean.getBackgroundWidth()); - intent.putExtra("aspectY", bean.getBackgroundHeight()); - } - - intent.putExtra("return-data", true); - intent.putExtra(MediaStore.EXTRA_OUTPUT, cropOutPutUri); - intent.putExtra("scale", true); - intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - startActivityForResult(intent, REQUEST_CROP_IMAGE); - } - - /** - * 保存剪裁后的Bitmap(优化版) - */ - private void saveCropBitmap(Bitmap bitmap) { - if (bitmap == null) { - ToastUtils.show("剪裁图片为空"); - return; - } - - // 内存优化:大图片自动缩放 - Bitmap scaledBitmap = bitmap; - if (bitmap.getByteCount() > 10 * 1024 * 1024) { // 超过10MB - float scale = 1.0f; - while (scaledBitmap.getByteCount() > 5 * 1024 * 1024) { - scale -= 0.2f; // 每次缩小20% - if (scale < 0.2f) break; // 最小缩放到20% - scaledBitmap = scaleBitmap(scaledBitmap, scale); - } - if (scaledBitmap != bitmap) { - bitmap.recycle(); // 回收原Bitmap - } - } - - // 优化:创建保存目录 - File backgroundDir = new File(mBackgroundPictureUtils.getBackgroundDir()); - if (!backgroundDir.exists()) { - if (!backgroundDir.mkdirs()) { - ToastUtils.show("无法创建保存目录"); - if (scaledBitmap != bitmap) scaledBitmap.recycle(); - return; - } - } - - File saveFile = new File(backgroundDir, getBackgroundFileName()); - - // 优化:检查文件是否可写 - if (saveFile.exists() && !saveFile.canWrite()) { - if (!saveFile.delete()) { - ToastUtils.show("无法删除旧文件"); - if (scaledBitmap != bitmap) scaledBitmap.recycle(); - return; - } - } - - FileOutputStream fos = null; - try { - fos = new FileOutputStream(saveFile); - boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos); - fos.flush(); - if (success) { - ToastUtils.show("保存成功"); - // 更新数据 - mBackgroundPictureUtils.getBackgroundPictureBean().setIsUseBackgroundFile(true); - updatePreviewBackground(); - } else { - ToastUtils.show("图片压缩保存失败"); - } - } catch (FileNotFoundException e) { - LogUtils.e(TAG, "文件未找到" + e); - ToastUtils.show("保存失败:文件路径错误"); - } catch (IOException e) { - LogUtils.e(TAG, "写入异常" + e); - ToastUtils.show("保存失败:磁盘可能已满或路径错误"); - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LogUtils.e(TAG, "流关闭异常" + e); - } - } - if (scaledBitmap != null && !scaledBitmap.isRecycled()) { - scaledBitmap.recycle(); - } - } - } - - /** - * 缩放Bitmap - */ - private Bitmap scaleBitmap(Bitmap original, float scale) { - if (original == null) { - return null; - } - int width = (int) (original.getWidth() * scale); - int height = (int) (original.getHeight() * scale); - return Bitmap.createScaledBitmap(original, width, height, true); - } - - /** - * 分享图片 - */ - void sharePicture() { - Uri uri = UriUtil.getUriForFile(this, mfRecivedPicture); - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, uri); - shareIntent.setType("image/" + _mszCommonFileType); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(Intent.createChooser(shareIntent, "Share Image")); - } - - public static File getRecivedPictureFile(Context context) { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(context); - utils.loadBackgroundPictureBean(); - return new File(utils.getBackgroundDir(), _RecivedPictureFileName); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_SELECT_PICTURE && resultCode == RESULT_OK) { - try { - Uri selectedImage = data.getData(); - LogUtils.d(TAG, "Uri is : " + selectedImage.toString()); - File fSrcImage = new File(UriUtil.getFilePathFromUri(this, selectedImage)); - mfRecivedPicture = getRecivedPictureFile(this); - if (FileUtils.copyFile(fSrcImage, mfRecivedPicture)) { - startCropImageActivity(false); - } else { - ToastUtils.show("图片复制失败,请重试"); - } - } catch (Exception e) { - LogUtils.e(TAG, "选择图片异常" + e); - ToastUtils.show("选择图片失败:" + e.getMessage()); - } - } else if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { - LogUtils.d(TAG, "REQUEST_TAKE_PHOTO"); - Bundle extras = data.getExtras(); - if (extras != null) { - Bitmap imageBitmap = (Bitmap) extras.get("data"); - if (imageBitmap != null) { - compressQualityToRecivedPicture(imageBitmap); - startCropImageActivity(false); - } else { - ToastUtils.show("拍照图片为空"); - } - } else { - ToastUtils.show("拍照数据获取失败"); - } - } else if (requestCode == REQUEST_CROP_IMAGE && resultCode == RESULT_OK) { - LogUtils.d(TAG, "CROP_IMAGE_REQUEST_CODE"); - try { - Bitmap cropBitmap = null; - // 方案1:通过Intent获取剪裁后的Bitmap - if (data != null && data.hasExtra("data")) { - cropBitmap = data.getParcelableExtra("data"); - } else if (mfTempCropPicture.exists()) { - cropBitmap = BitmapFactory.decodeFile(mfTempCropPicture.getPath()); - } else { - ToastUtils.show("剪裁文件不存在"); - return; - } - - if (cropBitmap != null) { - saveCropBitmap(cropBitmap); - } else { - ToastUtils.show("获取剪裁图片失败"); - } - } catch (OutOfMemoryError e) { - LogUtils.e(TAG, "内存溢出" + e); - ToastUtils.show("保存失败:内存不足,请尝试裁剪更小的图片"); - } catch (Exception e) { - LogUtils.e(TAG, "剪裁保存异常" + e); - ToastUtils.show("保存失败:" + e.getMessage()); - }/* finally { - // 安全删除临时文件 - if (mfTempCropPicture.exists()) { - mfTempCropPicture.delete(); - } - }*/ - } else if (resultCode != RESULT_OK) { - LogUtils.d(TAG, "操作取消或失败,requestCode: " + requestCode); - ToastUtils.show("操作已取消"); - } - } - - /** - * 检查类型是否为图片 - */ - private boolean isImageType(String type) { - return type.startsWith("image/") || "image/jpeg".equals(type) || - "image/jpg".equals(type) || "image/png".equals(type) || - "image/webp".equals(type); - } - - /** - * 检查并申请存储权限 - */ - private boolean checkAndRequestStoragePermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - STORAGE_PERMISSION_REQUEST); - return false; - } - } - return true; - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == STORAGE_PERMISSION_REQUEST) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - ToastUtils.show("存储权限已获取"); - } else { - ToastUtils.show("需要存储权限才能保存图片"); - } - } - } - - void setBackgroundColor() { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(BackgroundPictureActivity.this); - BackgroundPictureBean bean = utils.getBackgroundPictureBean(); - int nPixelColor = bean.getPixelColor(); - RelativeLayout mainLayout = findViewById(R.id.activitybackgroundpictureRelativeLayout1); - mainLayout.setBackgroundColor(nPixelColor); - } - - @Override - protected void onResume() { - 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/BackgroundSettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java new file mode 100644 index 0000000..cc31537 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/BackgroundSettingsActivity.java @@ -0,0 +1,771 @@ +package cc.winboll.studio.powerbell.activities; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.view.View; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.FileProvider; +import cc.winboll.studio.libaes.views.AToolbar; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.App; +import cc.winboll.studio.powerbell.R; +import cc.winboll.studio.powerbell.dialogs.BackgroundPicturePreviewDialog; +import cc.winboll.studio.powerbell.dialogs.YesNoAlertDialog; +import cc.winboll.studio.powerbell.model.BackgroundBean; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; +import cc.winboll.studio.powerbell.utils.ImageCropUtils; +import cc.winboll.studio.powerbell.utils.PermissionUtils; +import cc.winboll.studio.powerbell.views.BackgroundView; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class BackgroundSettingsActivity extends WinBoLLActivity implements BackgroundPicturePreviewDialog.IOnRecivedPictureListener { + + public static final String TAG = "BackgroundSettingsActivity"; + private BackgroundSourceUtils mBgSourceUtils; + private PermissionUtils mPermissionUtils; + + public static final int REQUEST_SELECT_PICTURE = 0; + public static final int REQUEST_TAKE_PHOTO = 1; + public static final int REQUEST_CROP_IMAGE = 2; + + private AToolbar mAToolbar; + private BackgroundView mBackgroundView; + private File mfTakePhoto; + volatile boolean isCommitSettings = false; + + @Override + public Activity getActivity() { + return this; + } + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_background_settings); + + mBackgroundView = (BackgroundView) findViewById(R.id.background_view); + mBgSourceUtils = BackgroundSourceUtils.getInstance(this); + mPermissionUtils = PermissionUtils.getInstance(); + + File tempDir = new File(App.getTempDirPath()); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + mfTakePhoto = new File(tempDir, "TakePhoto.jpg"); + + File selectTempDir = new File(mBgSourceUtils.getBackgroundSourceDirPath(), "SelectTemp"); + if (!selectTempDir.exists()) { + selectTempDir.mkdirs(); + LogUtils.d(TAG, "【选图初始化】选图临时目录创建完成:" + selectTempDir.getAbsolutePath()); + } + + initToolbar(); + initClickListeners(); + initBackgroundViewByPreviewBean(); + handleShareIntent(); + + LogUtils.d(TAG, "【初始化】BackgroundSettingsActivity 初始化完成"); + } + + private void initBackgroundViewByPreviewBean() { + LogUtils.d(TAG, "【Bean初始化】正式Bean → 预览Bean"); + mBgSourceUtils.setCurrentSourceToPreview(); + doubleRefreshPreview(); + LogUtils.d(TAG, "【Bean初始化】预览Bean初始化完成"); + } + + private void initToolbar() { + mAToolbar = (AToolbar) findViewById(R.id.toolbar); + setActionBar(mAToolbar); + mAToolbar.setSubtitle(R.string.subtitle_activity_backgroundpicture); + getActionBar().setDisplayHomeAsUpEnabled(true); + mAToolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【导航栏】点击返回"); + finish(); + } + }); + } + + private void initClickListeners() { + findViewById(R.id.activitybackgroundpictureAButton5).setOnClickListener(onOriginNullClickListener); + findViewById(R.id.activitybackgroundpictureAButton4).setOnClickListener(onReceivedPictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton1).setOnClickListener(onTakePhotoClickListener); + findViewById(R.id.activitybackgroundpictureAButton2).setOnClickListener(onSelectPictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton3).setOnClickListener(onCropPictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton6).setOnClickListener(onCropFreePictureClickListener); + findViewById(R.id.activitybackgroundpictureAButton7).setOnClickListener(onPixelPickerClickListener); + findViewById(R.id.activitybackgroundpictureAButton8).setOnClickListener(onCleanPixelClickListener); + } + + private void handleShareIntent() { + Intent intent = getIntent(); + if (intent != null) { + String action = intent.getAction(); + String type = intent.getType(); + if (Intent.ACTION_SEND.equals(action) && type != null && isImageType(type)) { + BackgroundPicturePreviewDialog dlg = new BackgroundPicturePreviewDialog(this); + dlg.show(); + LogUtils.d(TAG, "【分享处理】收到分享图片意图"); + } + } + } + + boolean isImageType(String lowerMimeType) { + return lowerMimeType.equals("image/jpeg") + || lowerMimeType.equals("image/png") + || lowerMimeType.equals("image/tiff") + || lowerMimeType.equals("image/jpg") + || lowerMimeType.equals("image/svg+xml"); + } + + @Override + public void onAcceptRecivedPicture(String szPreRecivedPictureName) { + ToastUtils.show("图片接收功能暂未实现"); + LogUtils.d(TAG, "【分享接收】图片名:" + szPreRecivedPictureName); + } + + private View.OnClickListener onOriginNullClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】取消背景图片"); + BackgroundBean previewBackgroundBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBackgroundBean.setIsUseBackgroundFile(false); + doubleRefreshPreview(); + } + }; + + private View.OnClickListener onSelectPictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】选择图片"); + if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { + LogUtils.d(TAG, "【选图权限】已获取"); + Intent[] intents = new Intent[3]; + Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); + getContentIntent.setType("image/*"); + getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); + getContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intents[0] = getContentIntent; + + Intent pickIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + pickIntent.setType("image/*"); + pickIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intents[1] = pickIntent; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Intent openDocIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + openDocIntent.setType("image/*"); + openDocIntent.addCategory(Intent.CATEGORY_OPENABLE); + openDocIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + intents[2] = openDocIntent; + } + + Intent validIntent = null; + for (int i = 0; i < intents.length; i++) { + Intent intent = intents[i]; + if (intent != null && intent.resolveActivity(getPackageManager()) != null) { + validIntent = intent; + break; + } + } + + if (validIntent != null) { + Intent chooser = Intent.createChooser(validIntent, "选择图片"); + chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(chooser, REQUEST_SELECT_PICTURE); + LogUtils.d(TAG, "【选图意图】启动图片选择"); + } else { + LogUtils.d(TAG, "【选图意图】无相册应用"); + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("未找到相册应用,请安装后重试"); + new AlertDialog.Builder(BackgroundSettingsActivity.this) + .setTitle("无图片选择应用") + .setMessage("需要安装相册应用才能选择图片") + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent marketIntent = new Intent(Intent.ACTION_VIEW); + marketIntent.setData(Uri.parse("market://details?id=com.android.gallery3d")); + if (marketIntent.resolveActivity(getPackageManager()) != null) { + startActivity(marketIntent); + } else { + ToastUtils.show("无法打开应用商店"); + } + } + }) + .setNegativeButton("取消", null) + .show(); + } + }); + } + } else { + LogUtils.d(TAG, "【选图权限】已申请"); + } + } + }; + + private View.OnClickListener onCropPictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】固定比例裁剪"); + // 调用裁剪工具类:传入上下文、预览图、固定比例(按视图宽高)、请求码 + ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE + ); + } + }; + + private View.OnClickListener onCropFreePictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】自由裁剪"); + // 调用裁剪工具类:传入上下文、预览图、自由裁剪(比例参数传0)、请求码 + ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, + mBgSourceUtils.getPreviewBackgroundBean(), + 0, + 0, + true, + REQUEST_CROP_IMAGE + ); + } + }; + + private View.OnClickListener onTakePhotoClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】拍照"); + if (mfTakePhoto.exists()) { + boolean deleteSuccess = mfTakePhoto.delete(); + LogUtils.d(TAG, "【拍照准备】清理旧文件:" + (deleteSuccess ? "成功" : "失败")); + } + try { + boolean createSuccess = mfTakePhoto.createNewFile(); + LogUtils.d(TAG, "【拍照准备】创建新文件:" + (createSuccess ? "成功" : "失败")); + if (!createSuccess) { + ToastUtils.show("拍照文件创建失败"); + return; + } + } catch (IOException e) { + LogUtils.e(TAG, "【拍照异常】" + e.getMessage()); + ToastUtils.show("拍照文件创建失败"); + return; + } + + if (mPermissionUtils.checkAndRequestStoragePermission(BackgroundSettingsActivity.this)) { + LogUtils.d(TAG, "【拍照权限】已获取"); + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + try { + Uri photoUri = getFileProviderUri(mfTakePhoto); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); + startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); + LogUtils.d(TAG, "【拍照启动】Uri:" + photoUri.toString()); + } catch (Exception e) { + String errMsg = "拍照启动异常:" + e.getMessage(); + ToastUtils.show(errMsg.substring(0, 20)); + LogUtils.e(TAG, "【拍照失败】"); + } + } else { + LogUtils.d(TAG, "【拍照权限】已申请"); + } + } + }; + + private View.OnClickListener onReceivedPictureClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + ToastUtils.show("图片接收功能暂未实现"); + LogUtils.d(TAG, "【按钮点击】图片接收"); + } + }; + + private View.OnClickListener onPixelPickerClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】像素拾取"); + String targetImagePath = mBgSourceUtils.getCurrentBackgroundFilePath(); + File targetFile = new File(targetImagePath); + if (targetFile == null || !targetFile.exists() || targetFile.length() <= 0) { + ToastUtils.show("无有效图片可拾取像素"); + LogUtils.e(TAG, "【像素拾取失败】"); + return; + } + Intent intent = new Intent(getApplicationContext(), PixelPickerActivity.class); + intent.putExtra("imagePath", targetImagePath); + startActivity(intent); + LogUtils.d(TAG, "【像素拾取启动】"); + } + }; + + private View.OnClickListener onCleanPixelClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + LogUtils.d(TAG, "【按钮点击】清空像素颜色"); + BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); + int oldColor = bean.getPixelColor(); + bean.setPixelColor(0); + mBgSourceUtils.saveSettings(); + doubleRefreshPreview(); + ToastUtils.show("像素颜色已清空"); + LogUtils.d(TAG, "【像素清空】旧颜色:" + oldColor); + } + }; + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + LogUtils.d(TAG, "【回调触发】requestCode:" + requestCode + ",resultCode:" + resultCode); + + try { + if (requestCode == PermissionUtils.REQUEST_MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + handleStoragePermissionCallback(); + return; + } + + if (resultCode != RESULT_OK) { + handleOperationCancelOrFail(); + return; + } + + switch (requestCode) { + case REQUEST_SELECT_PICTURE: + handleSelectPictureResult(resultCode, data); + break; + case REQUEST_TAKE_PHOTO: + handleTakePhotoResult(resultCode, data); + break; + case REQUEST_CROP_IMAGE: + handleCropImageResult(requestCode, resultCode, data); + break; + default: + LogUtils.d(TAG, "【回调忽略】未知requestCode"); + break; + } + } catch (Exception e) { + LogUtils.e(TAG, "【回调异常】" + e.getMessage()); + ToastUtils.show("操作失败"); + } + } + + private void handleStoragePermissionCallback() { + if (Environment.isExternalStorageManager()) { + LogUtils.d(TAG, "【权限回调】已授予"); + ToastUtils.show("存储权限已获取"); + } else { + LogUtils.d(TAG, "【权限回调】已拒绝"); + ToastUtils.show("存储权限不足"); + } + } + + private void handleTakePhotoResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } + + if (!mfTakePhoto.exists() || mfTakePhoto.length() <= 0) { + ToastUtils.show("拍照文件无效"); + return; + } + + Bitmap photoBitmap = getTakePhotoBitmap(data); + if (photoBitmap != null && !photoBitmap.isRecycled()) { + mBgSourceUtils.compressQualityToRecivedPicture(photoBitmap); + } else { + ToastUtils.show("拍照图片为空"); + return; + } + + mBgSourceUtils.saveFileToPreviewBean(mfTakePhoto, mfTakePhoto.getAbsolutePath()); + doubleRefreshPreview(); + + // 拍照后启动固定比例裁剪(调用工具类) + ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE + ); + LogUtils.d(TAG, "【拍照完成】已启动裁剪"); + } + + private Bitmap getTakePhotoBitmap(Intent data) { + LogUtils.d(TAG, "【拍照Bitmap解析】开始"); + if (mfTakePhoto != null && mfTakePhoto.exists()) { + LogUtils.d(TAG, "【拍照Bitmap解析】从文件解析"); + Bitmap photoBitmap = parseCropTempFileToBitmap(mfTakePhoto); + if (photoBitmap != null && !photoBitmap.isRecycled()) { + LogUtils.d(TAG, "【拍照Bitmap解析】成功"); + return photoBitmap; + } else { + LogUtils.w(TAG, "【拍照Bitmap解析】文件解析失败,尝试Intent"); + } + } else { + LogUtils.w(TAG, "【拍照Bitmap解析】文件无效,尝试Intent"); + } + + if (data != null) { + try { + Bitmap thumbnailBitmap = (Bitmap) data.getParcelableExtra("data"); + if (thumbnailBitmap != null && !thumbnailBitmap.isRecycled()) { + LogUtils.d(TAG, "【拍照Bitmap解析】从Intent获取成功"); + return thumbnailBitmap; + } else { + LogUtils.e(TAG, "【拍照Bitmap解析】Intent解析失败"); + } + } catch (Exception e) { + LogUtils.e(TAG, "【拍照Bitmap解析】Intent异常:" + e.getMessage()); + } + } else { + LogUtils.e(TAG, "【拍照Bitmap解析】Intent为空"); + } + + LogUtils.e(TAG, "【拍照Bitmap解析】失败"); + ToastUtils.show("拍照图片解析失败"); + return null; + } + + private void handleSelectPictureResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK || data == null) { + handleOperationCancelOrFail(); + return; + } + + Uri selectedImage = data.getData(); + if (selectedImage == null) { + ToastUtils.show("图片Uri为空"); + return; + } + LogUtils.d(TAG, "【选图回调】Uri : " + selectedImage.toString()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getContentResolver().takePersistableUriPermission( + selectedImage, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ); + LogUtils.d(TAG, "【选图权限】已添加持久化权限"); + } + + mBgSourceUtils.createCropFileProviderBackgroundBean(selectedImage); + + LogUtils.d(TAG, "【选图同步】路径绑定完成"); + // 选图后启动固定比例裁剪(调用工具类) + mBgSourceUtils.loadSettings(); + ImageCropUtils.startImageCrop(BackgroundSettingsActivity.this, + mBgSourceUtils.getPreviewBackgroundBean(), + mBackgroundView.getWidth(), + mBackgroundView.getHeight(), + false, + REQUEST_CROP_IMAGE + ); + } + + private void handleCropImageResult(int requestCode, int resultCode, Intent data) { + LogUtils.d(TAG, "handleCropImageResult: 处理裁剪结果"); + File cropTempFile = new File(mBgSourceUtils.getPreviewBackgroundBean().getBackgroundScaledCompressFilePath()); + boolean isFileExist = cropTempFile.exists(); + boolean isFileReadable = isFileExist ? cropTempFile.canRead() : false; + long fileSize = isFileExist ? cropTempFile.length() : 0; + boolean isCropSuccess = (resultCode == RESULT_OK) && isFileExist && isFileReadable && fileSize > 100; + + if (isCropSuccess) { + LogUtils.d(TAG, "handleCropImageResult: 裁剪成功"); + BackgroundBean previewBean = mBgSourceUtils.getPreviewBackgroundBean(); + previewBean.setIsUseBackgroundFile(true); + previewBean.setIsUseBackgroundScaledCompressFile(true); + mBgSourceUtils.saveSettings(); + + float systemFileRatio = getRatioFromSystemCropFile(cropTempFile); + if (systemFileRatio > 0) { + Bitmap cropBitmap = parseCropTempFileToBitmap(cropTempFile); + if (cropBitmap != null && !cropBitmap.isRecycled()) { + Bitmap scaledCropBitmap = adjustBitmapToFinalRatio(cropBitmap, systemFileRatio); + saveScaledBitmapToFile(scaledCropBitmap, cropTempFile); + + if (scaledCropBitmap != cropBitmap && !scaledCropBitmap.isRecycled()) { + scaledCropBitmap.recycle(); + } + if (!cropBitmap.isRecycled()) { + cropBitmap.recycle(); + } + } + } + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + if (mBackgroundView != null && !isFinishing()) { + mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); + LogUtils.d(TAG, "handleCropImageResult: 裁剪图片加载完成"); + } + } + }, 50); + } else { + handleOperationCancelOrFail(); + } + } + + private float getRatioFromSystemCropFile(File systemCropFile) { + LogUtils.d(TAG, "getRatioFromSystemCropFile: 读取比例"); + if (systemCropFile == null || !systemCropFile.exists() || !systemCropFile.isFile()) { + LogUtils.e(TAG, "getRatioFromSystemCropFile: 文件无效"); + return -1; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(systemCropFile.getAbsolutePath(), options); + + int cropWidth = options.outWidth; + int cropHeight = options.outHeight; + if (cropWidth <= 0 || cropHeight <= 0) { + LogUtils.e(TAG, "getRatioFromSystemCropFile: 尺寸无效"); + return -1; + } + + float systemRatio = (float) cropWidth / cropHeight; + LogUtils.d(TAG, "getRatioFromSystemCropFile: 比例:" + systemRatio); + return systemRatio; + } + + private Bitmap adjustBitmapToFinalRatio(Bitmap originalBitmap, float finalCropRatio) { + LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整比例"); + if (originalBitmap == null || originalBitmap.isRecycled() || finalCropRatio <= 0) { + LogUtils.e(TAG, "adjustBitmapToFinalRatio: 参数无效"); + return originalBitmap; + } + + int originalWidth = originalBitmap.getWidth(); + int originalHeight = originalBitmap.getHeight(); + float originalRatio = (float) originalWidth / originalHeight; + + if (Math.abs(originalRatio - finalCropRatio) < 0.001f) { + LogUtils.d(TAG, "adjustBitmapToFinalRatio: 比例一致"); + return originalBitmap; + } + + int targetWidth, targetHeight; + targetHeight = originalHeight; + targetWidth = Math.round(targetHeight * finalCropRatio); + if (targetWidth > originalWidth) { + targetWidth = originalWidth; + targetHeight = Math.round(targetWidth / finalCropRatio); + } + + targetWidth = Math.round(targetHeight * finalCropRatio); + LogUtils.d(TAG, "adjustBitmapToFinalRatio: 调整前:" + originalWidth + "x" + originalHeight + ",调整后:" + targetWidth + "x" + targetHeight); + + Bitmap adjustedBitmap = Bitmap.createBitmap( + originalBitmap, + (originalWidth - targetWidth) / 2, + (originalHeight - targetHeight) / 2, + targetWidth, + targetHeight + ); + + return adjustedBitmap; + } + + private void saveScaledBitmapToFile(Bitmap bitmap, File targetFile) { + LogUtils.d(TAG, "saveScaledBitmapToFile: 保存图片"); + if (bitmap == null || bitmap.isRecycled() || targetFile == null) { + LogUtils.e(TAG, "saveScaledBitmapToFile: 参数无效"); + return; + } + + if (targetFile.exists()) { + boolean deleteSuccess = targetFile.delete(); + LogUtils.d(TAG, "saveScaledBitmapToFile: 删除原文件:" + (deleteSuccess ? "成功" : "失败")); + } + + OutputStream outputStream = null; + try { + outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)); + bitmap.compress(CompressFormat.JPEG, 100, outputStream); + outputStream.flush(); + LogUtils.d(TAG, "saveScaledBitmapToFile: 保存成功"); + } catch (IOException e) { + LogUtils.e(TAG, "saveScaledBitmapToFile: 异常:" + e.getMessage()); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "saveScaledBitmapToFile: 关闭流异常"); + } + } + } + } + + private Bitmap parseCropTempFileToBitmap(File cropTempFile) { + LogUtils.d(TAG, "parseCropTempFileToBitmap: 解析文件"); + if (cropTempFile == null || !cropTempFile.exists() || !cropTempFile.isFile() || cropTempFile.length() <= 100) { + LogUtils.e(TAG, "parseCropTempFileToBitmap: 文件无效"); + return null; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + + int maxSize = 2048; + int sampleSize = 1; + while (options.outWidth / sampleSize > maxSize || options.outHeight / sampleSize > maxSize) { + sampleSize *= 2; + } + sampleSize = Math.min(sampleSize, 16); + LogUtils.d(TAG, "parseCropTempFileToBitmap: 采样率:" + sampleSize); + + options.inJustDecodeBounds = false; + options.inSampleSize = sampleSize; + options.inPreferredConfig = Bitmap.Config.RGB_565; + options.inPurgeable = true; + options.inInputShareable = true; + + Bitmap cropBitmap = null; + try { + cropBitmap = BitmapFactory.decodeFile(cropTempFile.getAbsolutePath(), options); + if (cropBitmap == null || cropBitmap.isRecycled()) { + LogUtils.e(TAG, "parseCropTempFileToBitmap: 解析失败"); + return null; + } + LogUtils.d(TAG, "parseCropTempFileToBitmap: 解析成功"); + } catch (OutOfMemoryError e) { + LogUtils.e(TAG, "parseCropTempFileToBitmap: OOM"); + Toast.makeText(this, "图片解析失败", Toast.LENGTH_SHORT).show(); + return null; + } catch (Exception e) { + LogUtils.e(TAG, "parseCropTempFileToBitmap: 异常:" + e.getMessage()); + return null; + } + + return cropBitmap; + } + + private void doubleRefreshPreview() { + LogUtils.d(TAG, "【双重刷新】开始"); + if (mBackgroundView != null && !isFinishing()) { + mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); + LogUtils.d(TAG, "【双重刷新】第一重完成"); + } else { + LogUtils.w(TAG, "【双重刷新】跳过"); + return; + } + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + if (mBackgroundView != null && !isFinishing()) { + mBackgroundView.loadBackgroundBean(mBgSourceUtils.getPreviewBackgroundBean()); + LogUtils.d(TAG, "【双重刷新】第二重完成"); + } + } + }, 50); + } + + + private void handleOperationCancelOrFail() { + initBackgroundViewByPreviewBean(); + LogUtils.d(TAG, "【操作回调】取消或失败"); + ToastUtils.show("操作已取消"); + } + + + /** + * 启动系统裁剪工具 + * @param activity 上下文 + * @param srcFile 裁剪原图 + * @param aspectX 裁剪宽比例(自由裁剪传0) + * @param aspectY 裁剪高比例(自由裁剪传0) + * @param isFreeCrop 是否自由裁剪 + * @param requestCode 裁剪请求码 + */ + /** + * 启动系统裁剪工具 + * @param activity 上下文 + * @param srcFile 裁剪原图 + * @param aspectX 裁剪宽比例(自由裁剪传0) + * @param aspectY 裁剪高比例(自由裁剪传0) + * @param isFreeCrop 是否自由裁剪 + * @param requestCode 裁剪请求码 + */ + + + /** + * 获取FileProvider Uri(复用方法,避免重复代码) + */ + public Uri getFileProviderUri(File file) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + String FILE_PROVIDER_AUTHORITY = getPackageName() + ".fileprovider"; + + return FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, file); + } else { + return Uri.fromFile(file); + } + } catch (Exception e) { + LogUtils.e(TAG, "getFileProviderUri: 生成Uri失败:" + e.getMessage()); + return null; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + LogUtils.d(TAG, "【权限回调】转发处理"); + mPermissionUtils.handleStoragePermissionResult(this, requestCode, permissions, grantResults); + } + + @Override + public void finish() { + if (isCommitSettings) { + super.finish(); + } else { + YesNoAlertDialog.show(this, "背景更换问题", "是否确定背景图片设置?", new YesNoAlertDialog.OnDialogResultListener(){ + @Override + public void onYes() { + mBgSourceUtils.commitPreviewSourceToCurrent(); + isCommitSettings = true; + finish(); + } + + @Override + public void onNo() { + isCommitSettings = true; + finish(); + } + }); + } + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/PixelPickerActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/PixelPickerActivity.java index 85fd970..8ae5aef 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/PixelPickerActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/PixelPickerActivity.java @@ -24,10 +24,10 @@ import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libaes.views.AToolbar; import cc.winboll.studio.libappbase.GlobalApplication; import cc.winboll.studio.powerbell.R; -import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity; +import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity; import cc.winboll.studio.powerbell.activities.PixelPickerActivity; -import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; -import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils; +import cc.winboll.studio.powerbell.model.BackgroundBean; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -193,10 +193,10 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi public void onClick(View v) { dialog.dismiss(); // 可以在这里添加确定后的回调逻辑 - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this); - BackgroundPictureBean bean = utils.getBackgroundPictureBean(); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this); + BackgroundBean bean = utils.getCurrentBackgroundBean(); bean.setPixelColor(pixelColor); - utils.saveData(); + utils.saveSettings(); Toast.makeText(PixelPickerActivity.this, "已记录像素值", Toast.LENGTH_SHORT).show(); setBackgroundColor(); } @@ -217,8 +217,8 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi void setBackgroundColor() { - BackgroundPictureUtils utils = BackgroundPictureUtils.getInstance(PixelPickerActivity.this); - BackgroundPictureBean bean = utils.getBackgroundPictureBean(); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(PixelPickerActivity.this); + BackgroundBean bean = utils.getCurrentBackgroundBean(); int nPixelColor = bean.getPixelColor(); RelativeLayout mainLayout = findViewById(R.id.activitypixelpickerRelativeLayout1); mainLayout.setBackgroundColor(nPixelColor); @@ -235,7 +235,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { Intent intent = new Intent(); - intent.setClass(this, BackgroundPictureActivity.class); + intent.setClass(this, BackgroundSettingsActivity.class); startActivity(intent); //GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), ); return true; @@ -248,7 +248,7 @@ public class PixelPickerActivity extends WinBoLLActivity implements IWinBoLLActi public void onBackPressed() { super.onBackPressed(); Intent intent = new Intent(); - intent.setClass(this, BackgroundPictureActivity.class); + intent.setClass(this, BackgroundSettingsActivity.class); startActivity(intent); //GlobalApplication.getWinBoLLActivityManager().startWinBoLLActivity(getApplicationContext(), BackgroundPictureActivity.class); } diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java new file mode 100644 index 0000000..75d979a --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/activities/SettingsActivity.java @@ -0,0 +1,31 @@ +package cc.winboll.studio.powerbell.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.R; +import cc.winboll.studio.powerbell.utils.PermissionUtils; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/11/27 14:26 + * @Describe 应用设置窗口 + */ +public class SettingsActivity extends Activity { + + public static final String TAG = "SettingsActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + } + + public void onCheckPermission(View view) { + //ToastUtils.show("onCheckPermission"); + if(PermissionUtils.getInstance().checkAndRequestStoragePermission(this)) { + ToastUtils.show("【权限检查】存储权限已全部获取"); + } + } +} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BackgroundPictureBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BackgroundPictureBean.java deleted file mode 100644 index d2aa922..0000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BackgroundPictureBean.java +++ /dev/null @@ -1,99 +0,0 @@ -package cc.winboll.studio.powerbell.beans; - -/** - * @Author ZhanGSKen - * @Date 2024/07/18 11:52:28 - * @Describe 应用背景图片数据类 - */ -import android.util.JsonReader; -import android.util.JsonWriter; -import cc.winboll.studio.libappbase.BaseBean; -import java.io.IOException; - -public class BackgroundPictureBean extends BaseBean { - - public static final String TAG = "BackgroundPictureBean"; - - int backgroundWidth = 100; - int backgroundHeight = 100; - boolean isUseBackgroundFile = false; - // 图片拾取像素颜色 - int pixelColor = 0; - - public BackgroundPictureBean() { - } - - public BackgroundPictureBean(String recivedFileName, boolean isUseBackgroundFile) { - this.isUseBackgroundFile = isUseBackgroundFile; - } - - public void setPixelColor(int pixelColor) { - this.pixelColor = pixelColor; - } - - public int getPixelColor() { - return pixelColor; - } - - public void setBackgroundWidth(int backgroundWidth) { - this.backgroundWidth = backgroundWidth; - } - - public int getBackgroundWidth() { - return backgroundWidth; - } - - public void setBackgroundHeight(int backgroundHeight) { - this.backgroundHeight = backgroundHeight; - } - - public int getBackgroundHeight() { - return backgroundHeight; - } - - public void setIsUseBackgroundFile(boolean isUseBackgroundFile) { - this.isUseBackgroundFile = isUseBackgroundFile; - } - - public boolean isUseBackgroundFile() { - return isUseBackgroundFile; - } - - @Override - public String getName() { - return BackgroundPictureBean.class.getName(); - } - - @Override - public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { - super.writeThisToJsonWriter(jsonWriter); - BackgroundPictureBean bean = this; - jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth()); - jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight()); - jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile()); - jsonWriter.name("pixelColor").value(bean.getPixelColor()); - } - - @Override - public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { - BackgroundPictureBean bean = new BackgroundPictureBean(); - jsonReader.beginObject(); - while (jsonReader.hasNext()) { - String name = jsonReader.nextName(); - if (name.equals("backgroundWidth")) { - bean.setBackgroundWidth(jsonReader.nextInt()); - } else if (name.equals("backgroundHeight")) { - bean.setBackgroundHeight(jsonReader.nextInt()); - } else if (name.equals("isUseBackgroundFile")) { - bean.setIsUseBackgroundFile(jsonReader.nextBoolean()); - } else if (name.equals("pixelColor")) { - bean.setPixelColor(jsonReader.nextInt()); - } else { - jsonReader.skipValue(); - } - } - // 结束 JSON 对象 - jsonReader.endObject(); - return bean; - } -} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java index ef7a444..cc89fd1 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/BackgroundPicturePreviewDialog.java @@ -12,8 +12,8 @@ import android.widget.Toast; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.powerbell.MainActivity; import cc.winboll.studio.powerbell.R; -import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity; -import cc.winboll.studio.powerbell.utils.BackgroundPictureUtils; +import cc.winboll.studio.powerbell.activities.BackgroundSettingsActivity; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.FileUtils; import cc.winboll.studio.powerbell.utils.UriUtil; import java.io.File; @@ -29,7 +29,7 @@ public class BackgroundPicturePreviewDialog extends Dialog { public static final String TAG = "BackgroundPicturePreviewDialog"; Context mContext; - BackgroundPictureUtils mBackgroundPictureUtils; + BackgroundSourceUtils mBackgroundPictureUtils; Button dialogbackgroundpicturepreviewButton1; Button dialogbackgroundpicturepreviewButton2; String mszPreReceivedFileName; @@ -40,7 +40,7 @@ public class BackgroundPicturePreviewDialog extends Dialog { initEnv(); mContext = context; - mBackgroundPictureUtils = ((BackgroundPictureActivity)context).mBackgroundPictureUtils; + mBackgroundPictureUtils = BackgroundSourceUtils.getInstance(mContext); ImageView imageView = findViewById(R.id.dialogbackgroundpicturepreviewImageView1); copyAndViewRecivePicture(imageView); @@ -78,7 +78,7 @@ public class BackgroundPicturePreviewDialog extends Dialog { void copyAndViewRecivePicture(ImageView imageView) { //AppConfigUtils appConfigUtils = AppConfigUtils.getInstance((GlobalApplication)mContext.getApplicationContext()); - BackgroundPictureActivity activity = ((BackgroundPictureActivity)mContext); + BackgroundSettingsActivity activity = ((BackgroundSettingsActivity)mContext); //取出文件uri Uri uri = activity.getIntent().getData(); @@ -95,7 +95,7 @@ public class BackgroundPicturePreviewDialog extends Dialog { File fSrcImage = new File(szSrcImage); //mszPreReceivedFileName = DateUtils.getDateNowString() + "-" + fSrcImage.getName(); - File mfPreReceivedPhoto = new File(activity.mBackgroundPictureUtils.getBackgroundDir(), mszPreReceivedFileName); + File mfPreReceivedPhoto = new File(BackgroundSourceUtils.getInstance(mContext).getBackgroundSourceDirPath(), mszPreReceivedFileName); // 复制源图片到剪裁文件 try { FileUtils.copyFileUsingFileChannels(fSrcImage, mfPreReceivedPhoto); 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 index 28bb94c..1627d0b 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/NetworkBackgroundDialog.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/dialogs/NetworkBackgroundDialog.java @@ -15,11 +15,12 @@ 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; +import cc.winboll.studio.powerbell.utils.ImageDownloader; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; /** * @Author ZhanGSKen&豆包大模型 @@ -41,15 +42,17 @@ public class NetworkBackgroundDialog extends AlertDialog { private Button btnConfirm; private Button btnPreview; private EditText etURL; - BackgroundView bvBackgroundPreview; + BackgroundView mBackgroundView; Context mContext; // 主线程 Handler,用于接收子线程消息并更新 UI private Handler mUiHandler; - String previewFilePath; + String mPreviewFilePath; + String mPreviewFileUrl; + String mDownloadSavedPath; // 按钮点击回调接口(Java7 接口实现) public interface OnDialogClickListener { - void onConfirm(); // 确认按钮点击 + void onConfirm(String szConfirmFilePath, String previewFileUrl); // 确认按钮点击 void onCancel(); // 取消按钮点击 } @@ -87,12 +90,12 @@ public class NetworkBackgroundDialog extends AlertDialog { switch (msg.what) { case MSG_IMAGE_LOAD_SUCCESS: // 图片加载成功,获取文件路径并设置背景 - String filePath = (String) msg.obj; - setBackgroundFromPath(filePath); + mDownloadSavedPath = (String) msg.obj; + previewBackground(mDownloadSavedPath); break; case MSG_IMAGE_LOAD_FAILED: // 图片加载失败,设置默认背景 - bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); + mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); ToastUtils.show("图片预览失败,请检查链接"); break; } @@ -134,8 +137,9 @@ public class NetworkBackgroundDialog extends AlertDialog { 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); - + mBackgroundView = (BackgroundView) dialogView.findViewById(R.id.bv_background_preview); + // 加载初始图片 + mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); // 设置按钮点击事件 setButtonClickListeners(); } @@ -149,6 +153,9 @@ public class NetworkBackgroundDialog extends AlertDialog { @Override public void onClick(View v) { LogUtils.d("NetworkBackgroundDialog", "取消按钮点击"); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext); + utils.setCurrentSourceToPreview(); + dismiss(); // 关闭对话框 if (listener != null) { listener.onCancel(); @@ -162,11 +169,12 @@ public class NetworkBackgroundDialog extends AlertDialog { public void onClick(View v) { LogUtils.d("NetworkBackgroundDialog", "确认按钮点击"); // 确定预览背景资源 - bvBackgroundPreview.saveToBackgroundSources(previewFilePath); + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext); + utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl); dismiss(); // 关闭对话框 if (listener != null) { - listener.onConfirm(); + listener.onConfirm(mPreviewFilePath, mPreviewFileUrl); } } }); @@ -175,14 +183,7 @@ public class NetworkBackgroundDialog extends AlertDialog { 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);*/ } }); } @@ -191,26 +192,25 @@ public class NetworkBackgroundDialog extends AlertDialog { * 根据文件路径设置 BackgroundView 背景(主线程调用) * @param filePath 图片文件路径 */ - private void setBackgroundFromPath(String filePath) { + private void previewBackground(String previewFilePath) { FileInputStream fis = null; try { - File imageFile = new File(filePath); + File imageFile = new File(previewFilePath); if (!imageFile.exists()) { - LogUtils.e(TAG, "图片文件不存在:" + filePath); - ToastUtils.show("Test"); - //bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); + ToastUtils.show("图片文件不存在:" + previewFilePath); + LogUtils.e(TAG, "图片文件不存在:" + previewFilePath); + mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); return; } // 预览背景 - previewFilePath = filePath; - bvBackgroundPreview.previewBackgroundImage(previewFilePath); - - LogUtils.d(TAG, "图片预览成功:" + filePath); - + mPreviewFilePath = previewFilePath; + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(mContext); + utils.saveFileToPreviewBean(new File(mPreviewFilePath), mPreviewFileUrl); + mBackgroundView.loadBackgroundBean(utils.getPreviewBackgroundBean()); } catch (Exception e) { e.printStackTrace(); - bvBackgroundPreview.setBackgroundResource(R.drawable.ic_launcher); + mBackgroundView.setBackgroundResource(R.drawable.ic_launcher); LogUtils.e(TAG, "图片预览失败:" + e.getMessage()); } finally { // Java7 手动关闭流,避免资源泄漏 @@ -249,40 +249,20 @@ public class NetworkBackgroundDialog extends AlertDialog { 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(){ + //String previewFileUrl = "https://example.com/test.jpg"; + mPreviewFileUrl = etURL.getText().toString(); + ImageDownloader.getInstance(mContext).downloadImage(mPreviewFileUrl, new ImageDownloader.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()); + public void onFailure(String errorMsg) { + ToastUtils.show("下载失败:" + errorMsg); } }); 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 10c6ae4..eb671e4 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 @@ -19,8 +19,11 @@ 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.PixelPickerActivity; +import cc.winboll.studio.powerbell.model.BackgroundBean; import cc.winboll.studio.powerbell.services.ControlCenterService; import cc.winboll.studio.powerbell.utils.AppConfigUtils; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; import cc.winboll.studio.powerbell.utils.ServiceUtils; import cc.winboll.studio.powerbell.views.BackgroundView; import cc.winboll.studio.powerbell.views.BatteryDrawable; @@ -44,6 +47,8 @@ public class MainViewFragment extends Fragment { Switch mswIsEnableService; TextView mtvTips; + private BackgroundSourceUtils mBgSourceUtils; + // 背景布局 //LinearLayout mLinearLayoutloadBackground; @@ -68,7 +73,7 @@ public class MainViewFragment extends Fragment { TextView mtvUsegeReminderValue; CheckBox mcbUsegeReminderValue; TextView mtvCurrentValue; - BackgroundView bvPreviewBackground; + BackgroundView mBackgroundView; @Override @@ -76,9 +81,11 @@ public class MainViewFragment extends Fragment { mView = inflater.inflate(R.layout.fragment_mainview, container, false); _mMainViewFragment = MainViewFragment.this; mAppConfigUtils = App.getAppConfigUtils(getActivity()); - + mBgSourceUtils = BackgroundSourceUtils.getInstance(getActivity()); // 获取指定ID的View实例 - bvPreviewBackground = mView.findViewById(R.id.fragmentmainviewBackgroundView1); + mBackgroundView = mView.findViewById(R.id.fragmentmainviewBackgroundView1); + + loadBackground(); /*final View mainImageView = mView.findViewById(R.id.fragmentmainviewImageView1); // 注册OnGlobalLayoutListener @@ -141,6 +148,19 @@ public class MainViewFragment extends Fragment { return mView; } + void loadBackground() { + BackgroundBean bean = mBgSourceUtils.getCurrentBackgroundBean(); + mBackgroundView.loadBackgroundBean(bean); + } + + @Override + public void onResume() { + super.onResume(); + loadBackground(); + } + + + void setViewData() { int nChargeReminderValue = mAppConfigUtils.getChargeReminderValue(); int nUsegeReminderValue = mAppConfigUtils.getUsegeReminderValue(); @@ -301,22 +321,10 @@ public class MainViewFragment extends Fragment { } 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); -// } + BackgroundSourceUtils utils = BackgroundSourceUtils.getInstance(getActivity()); + mBgSourceUtils.loadSettings(); + BackgroundBean bean = utils.getCurrentBackgroundBean(); + mBackgroundView.loadBackgroundBean(bean); } Handler mHandler = new Handler(){ diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/AppConfigBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/AppConfigBean.java similarity index 98% rename from powerbell/src/main/java/cc/winboll/studio/powerbell/beans/AppConfigBean.java rename to powerbell/src/main/java/cc/winboll/studio/powerbell/model/AppConfigBean.java index da4b831..1176432 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/AppConfigBean.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/AppConfigBean.java @@ -1,4 +1,4 @@ -package cc.winboll.studio.powerbell.beans; +package cc.winboll.studio.powerbell.model; /** * @Author ZhanGSKen diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java new file mode 100644 index 0000000..33f6ef2 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BackgroundBean.java @@ -0,0 +1,254 @@ +package cc.winboll.studio.powerbell.model; + +import android.util.JsonReader; +import android.util.JsonWriter; +import cc.winboll.studio.libappbase.BaseBean; +import java.io.IOException; + +/** + * @Author ZhanGSKen + * @Date 2024/07/18 11:52:28 + * @Describe 应用背景图片数据类(存储正式/预览背景配置,支持JSON序列化/反序列化) + */ +public class BackgroundBean extends BaseBean { + + public static final String TAG = "BackgroundPictureBean"; + + // 核心字段:背景图片文件名(对应应用私有目录下的图片文件,与BackgroundSettingsActivity的_mSourceCroppedFile匹配) + private String backgroundFileName = ""; + // 核心字段:背景图片完整路径(解决仅存文件名导致的路径拼接错误,与backgroundScaledCompressFilePath对应) + private String backgroundFilePath = ""; + // 附加字段:图片信息(如Uri、网络地址等,仅作备注,不参与路径生成) + private String backgroundFileInfo = ""; + // 控制字段:是否启用背景图片(true-显示背景图,false-显示透明背景) + private boolean isUseBackgroundFile = false; + // 核心字段:压缩后背景图片文件名(对应应用私有目录下的压缩图片,与saveCropBitmap的压缩图匹配) + private String backgroundScaledCompressFileName = ""; + // 核心字段:压缩后背景图片完整路径(解决仅存文件名导致的路径拼接错误,适配BackgroundSettingsActivity的私有目录) + private String backgroundScaledCompressFilePath = ""; + // 重命名字段:是否启用压缩背景图(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile,语义更清晰) + private boolean isUseBackgroundScaledCompressFile = false; + // 裁剪比例字段:背景图宽高比(默认1:1,用于固定比例裁剪) + private int backgroundWidth = 100; + private int backgroundHeight = 100; + // 像素拾取字段:拾取的像素颜色(用于纯色背景) + private int pixelColor = 0; + + /** + * 无参构造器(必须,JSON反序列化时需默认构造器) + */ + public BackgroundBean() { + } + + // ====================================== Getter/Setter 方法(全字段,含重命名+新增字段)====================================== + public String getBackgroundFileName() { + return backgroundFileName; + } + + public void setBackgroundFileName(String backgroundFileName) { + this.backgroundFileName = backgroundFileName == null ? "" : backgroundFileName; // 防null,避免空指针 + } + + public String getBackgroundFilePath() { + return backgroundFilePath; + } + + public void setBackgroundFilePath(String backgroundFilePath) { + this.backgroundFilePath = backgroundFilePath == null ? "" : backgroundFilePath; // 防null,避免路径拼接错误 + } + + public String getBackgroundFileInfo() { + return backgroundFileInfo; + } + + public void setBackgroundFileInfo(String backgroundFileInfo) { + this.backgroundFileInfo = backgroundFileInfo == null ? "" : backgroundFileInfo; // 防null,避免空指针 + } + + public boolean isUseBackgroundFile() { + return isUseBackgroundFile; + } + + public void setIsUseBackgroundFile(boolean isUseBackgroundFile) { + this.isUseBackgroundFile = isUseBackgroundFile; + } + + public String getBackgroundScaledCompressFileName() { + return backgroundScaledCompressFileName; + } + + public void setBackgroundScaledCompressFileName(String backgroundScaledCompressFileName) { + this.backgroundScaledCompressFileName = backgroundScaledCompressFileName == null ? "" : backgroundScaledCompressFileName; // 防null + } + + public String getBackgroundScaledCompressFilePath() { + return backgroundScaledCompressFilePath; + } + + public void setBackgroundScaledCompressFilePath(String backgroundScaledCompressFilePath) { + this.backgroundScaledCompressFilePath = backgroundScaledCompressFilePath == null ? "" : backgroundScaledCompressFilePath; // 防null,避免路径错误 + } + + /** + * 重命名:原isUseScaledCompress → 新isUseBackgroundScaledCompressFile(Getter/Setter同步修改) + * 语义:明确表示“是否启用背景压缩图文件”,避免与其他压缩逻辑混淆 + */ + public boolean isUseBackgroundScaledCompressFile() { + return isUseBackgroundScaledCompressFile; + } + + public void setIsUseBackgroundScaledCompressFile(boolean isUseBackgroundScaledCompressFile) { + this.isUseBackgroundScaledCompressFile = isUseBackgroundScaledCompressFile; + } + + public int getBackgroundWidth() { + return backgroundWidth; + } + + public void setBackgroundWidth(int backgroundWidth) { + this.backgroundWidth = backgroundWidth <= 0 ? 100 : backgroundWidth; // 防无效值,确保宽高比有效 + } + + public int getBackgroundHeight() { + return backgroundHeight; + } + + public void setBackgroundHeight(int backgroundHeight) { + this.backgroundHeight = backgroundHeight <= 0 ? 100 : backgroundHeight; // 防无效值,确保宽高比有效 + } + + public int getPixelColor() { + return pixelColor; + } + + public void setPixelColor(int pixelColor) { + this.pixelColor = pixelColor; + } + + // ====================================== 序列化/反序列化方法(适配重命名字段,兼容旧版本)====================================== + @Override + public String getName() { + return BackgroundBean.class.getName(); // 必须重写,BaseBean序列化时需类名标识 + } + + /** + * 序列化:同步重命名字段(原isUseScaledCompress → 新isUseBackgroundScaledCompressFile) + * 确保新字段能正常持久化,同时兼容旧版本JSON(可选:保留旧字段写入,避免旧版本读取异常) + */ + @Override + public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { + super.writeThisToJsonWriter(jsonWriter); + BackgroundBean bean = this; + jsonWriter.name("backgroundFileName").value(bean.getBackgroundFileName()); + jsonWriter.name("backgroundFilePath").value(bean.getBackgroundFilePath()); // 新增字段:背景原图完整路径 + jsonWriter.name("backgroundFileInfo").value(bean.getBackgroundFileInfo()); + jsonWriter.name("isUseBackgroundFile").value(bean.isUseBackgroundFile()); + jsonWriter.name("backgroundScaledCompressFileName").value(bean.getBackgroundScaledCompressFileName()); + jsonWriter.name("backgroundScaledCompressFilePath").value(bean.getBackgroundScaledCompressFilePath()); + // 关键:新字段序列化(核心) + jsonWriter.name("isUseBackgroundScaledCompressFile").value(bean.isUseBackgroundScaledCompressFile()); + // 兼容旧版本:保留旧字段名写入(可选,避免旧版本Bean读取时缺失字段) + jsonWriter.name("isUseScaledCompress").value(bean.isUseBackgroundScaledCompressFile()); + jsonWriter.name("backgroundWidth").value(bean.getBackgroundWidth()); + jsonWriter.name("backgroundHeight").value(bean.getBackgroundHeight()); + jsonWriter.name("pixelColor").value(bean.getPixelColor()); + } + + /** + * 反序列化:同步处理重命名字段(兼容旧版本JSON,新旧字段都能读取) + * 逻辑:优先读取新字段,若新字段不存在则读取旧字段(确保升级后旧配置仍有效) + */ + @Override + public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { + BackgroundBean bean = new BackgroundBean(); + jsonReader.beginObject(); + // 临时变量:存储旧字段值(用于兼容) + boolean tempUseScaledCompress = false; + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + switch (name) { + case "backgroundFileName": + bean.setBackgroundFileName(jsonReader.nextString()); + break; + case "backgroundFilePath": + bean.setBackgroundFilePath(jsonReader.nextString()); // 新增字段:读取背景原图完整路径 + break; + case "backgroundFileInfo": + bean.setBackgroundFileInfo(jsonReader.nextString()); + break; + case "isUseBackgroundFile": + bean.setIsUseBackgroundFile(jsonReader.nextBoolean()); + break; + case "backgroundScaledCompressFileName": + bean.setBackgroundScaledCompressFileName(jsonReader.nextString()); + break; + case "backgroundScaledCompressFilePath": + bean.setBackgroundScaledCompressFilePath(jsonReader.nextString()); + break; + // 关键:读取新字段(优先) + case "isUseBackgroundScaledCompressFile": + bean.setIsUseBackgroundScaledCompressFile(jsonReader.nextBoolean()); + break; + // 兼容旧版本:读取旧字段(若新字段未读取,则用旧字段值) + case "isUseScaledCompress": + tempUseScaledCompress = jsonReader.nextBoolean(); + break; + case "backgroundWidth": + bean.setBackgroundWidth(jsonReader.nextInt()); + break; + case "backgroundHeight": + bean.setBackgroundHeight(jsonReader.nextInt()); + break; + case "pixelColor": + bean.setPixelColor(jsonReader.nextInt()); + break; + default: + jsonReader.skipValue(); // 跳过未知字段,兼容旧版本Bean(避免崩溃) + break; + } + } + jsonReader.endObject(); + // 兼容逻辑:若新字段未被赋值(旧版本JSON无此字段),则用旧字段值填充 + if (!jsonReader.toString().contains("isUseBackgroundScaledCompressFile")) { + bean.setIsUseBackgroundScaledCompressFile(tempUseScaledCompress); + } + return bean; + } + + // ====================================== 辅助方法(同步更新重命名字段)====================================== + /** + * 重置背景配置(适配“取消背景”功能,同步重置重命名字段) + */ + public void resetBackgroundConfig() { + this.backgroundFileName = ""; + this.backgroundFilePath = ""; // 新增:重置背景原图完整路径 + this.backgroundScaledCompressFileName = ""; + this.backgroundScaledCompressFilePath = ""; + this.backgroundFileInfo = ""; + this.isUseBackgroundFile = false; + this.isUseBackgroundScaledCompressFile = false; // 重命名字段:重置为false + this.backgroundWidth = 100; + this.backgroundHeight = 100; + } + + /** + * 检查背景配置是否有效(适配BackgroundSettingsActivity的预览/保存校验) + * 同步使用重命名字段判断压缩图是否启用 + * @return true-配置有效(可显示背景图),false-配置无效 + */ + public boolean isBackgroundConfigValid() { + // 启用背景图时,需确保:原图路径/文件名 或 压缩图路径/文件名 非空 + if (!isUseBackgroundFile) { + return false; + } + // 原图校验:路径非空 或 文件名非空 + boolean isOriginalValid = !backgroundFilePath.isEmpty() || !backgroundFileName.isEmpty(); + // 压缩图校验:启用压缩图时,路径/文件名需非空 + boolean isCompressValid = true; + if (isUseBackgroundScaledCompressFile()) { // 重命名字段:判断是否启用压缩图 + isCompressValid = !backgroundScaledCompressFilePath.isEmpty() || !backgroundScaledCompressFileName.isEmpty(); + } + // 逻辑:启用压缩图则需压缩图有效;不启用压缩图则需原图有效 + return isUseBackgroundScaledCompressFile() ? isCompressValid : isOriginalValid; + } +} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BatteryData.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BatteryData.java similarity index 94% rename from powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BatteryData.java rename to powerbell/src/main/java/cc/winboll/studio/powerbell/model/BatteryData.java index 269a494..51b23dd 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BatteryData.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BatteryData.java @@ -1,4 +1,4 @@ -package cc.winboll.studio.powerbell.beans; +package cc.winboll.studio.powerbell.model; /** * @Author ZhanGSKen diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BatteryInfoBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BatteryInfoBean.java similarity index 97% rename from powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BatteryInfoBean.java rename to powerbell/src/main/java/cc/winboll/studio/powerbell/model/BatteryInfoBean.java index 3063c88..e7f0fc0 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/BatteryInfoBean.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/BatteryInfoBean.java @@ -1,4 +1,4 @@ -package cc.winboll.studio.powerbell.beans; +package cc.winboll.studio.powerbell.model; import android.util.JsonReader; import android.util.JsonWriter; diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/ControlCenterServiceBean.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/ControlCenterServiceBean.java similarity index 97% rename from powerbell/src/main/java/cc/winboll/studio/powerbell/beans/ControlCenterServiceBean.java rename to powerbell/src/main/java/cc/winboll/studio/powerbell/model/ControlCenterServiceBean.java index 097cade..5f13632 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/ControlCenterServiceBean.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/ControlCenterServiceBean.java @@ -1,4 +1,4 @@ -package cc.winboll.studio.powerbell.beans; +package cc.winboll.studio.powerbell.model; /** * @Author ZhanGSKen diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/NotificationMessage.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/NotificationMessage.java similarity index 94% rename from powerbell/src/main/java/cc/winboll/studio/powerbell/beans/NotificationMessage.java rename to powerbell/src/main/java/cc/winboll/studio/powerbell/model/NotificationMessage.java index 25e195f..b95b8f4 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/beans/NotificationMessage.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/model/NotificationMessage.java @@ -1,4 +1,4 @@ -package cc.winboll.studio.powerbell.beans; +package cc.winboll.studio.powerbell.model; // 应用消息结构 // 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 deleted file mode 100644 index 10f820f..0000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/BackgroundViewTestFragment.java +++ /dev/null @@ -1,48 +0,0 @@ -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 index a5c9124..367785d 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/unittest/MainUnitTestActivity.java @@ -1,14 +1,23 @@ package cc.winboll.studio.powerbell.unittest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; -import android.widget.FrameLayout; +import android.os.Environment; +import android.view.View; +import android.widget.Button; 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 androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.MainActivity; +import cc.winboll.studio.powerbell.R; +import cc.winboll.studio.powerbell.utils.BackgroundSourceUtils; +import cc.winboll.studio.powerbell.utils.ImageCropUtils; +import cc.winboll.studio.powerbell.views.BackgroundView; +import java.io.File; /** * @Author ZhanGSKen&豆包大模型 @@ -18,22 +27,157 @@ import cc.winboll.studio.libappbase.ToastUtils; public class MainUnitTestActivity extends AppCompatActivity { public static final String TAG = "MainUnitTestActivity"; - + public static final int REQUEST_CROP_IMAGE = 0; + // 新增:权限请求码 + public static final int REQUEST_STORAGE_PERMISSION = 1001; + View mainView; + BackgroundSourceUtils mBgSourceUtils; + BackgroundView mBackgroundView; + // 测试图片路径(用Environment获取,适配低版本,避免硬编码) + String szTestSource = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + + "/PowerBell/unittest/2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // 非调试状态就退出 - if (!GlobalApplication.isDebugging()) { - finish(); - } + + mBgSourceUtils = BackgroundSourceUtils.getInstance(this); + mBgSourceUtils.loadSettings(); + setContentView(R.layout.activity_mainunittest); - FragmentManager fragmentManager = getSupportFragmentManager(); - FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); - fragmentTransaction.add(R.id.activitymainunittestFrameLayout1, new BackgroundViewTestFragment(), BackgroundViewTestFragment.TAG); - fragmentTransaction.commit(); + mBackgroundView = findViewById(R.id.backgroundview); + + ((Button)findViewById(R.id.btn_main_activity)).setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View v) { + startActivity(new Intent(MainUnitTestActivity.this, MainActivity.class)); + } + }); + + // 裁剪测试按钮点击事件(新增权限校验) + ((Button)findViewById(R.id.btn_test_cropimage)).setOnClickListener(new View.OnClickListener(){ + @Override + public void onClick(View v) { + ToastUtils.show("onClick:准备启动裁剪"); + LogUtils.d(TAG, "【裁剪测试】点击裁剪按钮,校验权限"); + + // 修复1:移除高版本API依赖,适配低版本存储权限校验 + if (checkStoragePermission()) { + // 权限已授予,启动裁剪 + startCropTest(); + } else { + // 权限未授予,申请权限 + requestStoragePermission(); + } + } + }); - ToastUtils.show(String.format("%s onCreate", TAG)); + ToastUtils.show(String.format("%s onCreate", TAG)); + // 加载测试图片(验证图片路径是否有效) + 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() + + "/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.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.0(API 23)及以上,用通用的读写权限(移除高版本API) + return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * 申请存储读写权限(适配低版本SDK,移除READ_MEDIA_IMAGES依赖) + */ + private void requestStoragePermission() { + LogUtils.d(TAG, "【裁剪测试】申请存储读写权限"); + // 用通用的读写权限(适配所有Android 6.0+ 机型,无高版本依赖) + ActivityCompat.requestPermissions( + this, + new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, + REQUEST_STORAGE_PERMISSION + ); + } + + /** + * 权限申请回调 + */ + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == REQUEST_STORAGE_PERMISSION) { + // 校验权限是否授予 + boolean allGranted = true; + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + if (allGranted) { + ToastUtils.show("存储权限已授予,启动裁剪"); + startCropTest(); // 权限授予后启动裁剪 + } else { + ToastUtils.show("存储权限被拒绝,无法启动裁剪"); + LogUtils.e(TAG, "【裁剪测试】存储权限被拒绝"); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + LogUtils.d(TAG, "【裁剪回调】requestCode:" + requestCode + ",resultCode:" + resultCode + ",data:" + (data == null ? "null" : data.toString())); + ToastUtils.show(String.format("requestCode %d, resultCode %d, data is %s",requestCode, resultCode, data == null)); + // 裁剪完成后回收权限 + if (requestCode == REQUEST_CROP_IMAGE) { + String dstOutputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + + "/PowerBell/unittest/SelectCompress_2ae9dc9e-7a73-49d4-840a-7ff1712d868c1764798674763.jpeg"; + //Uri outputUri = ImageCropUtils.getFileProviderUriPublic(this, new File(dstOutputPath)); + //ImageCropUtils.releaseCropPermission(this, outputUri); + mBackgroundView.loadImage(dstOutputPath); + } + } + + void loadBackground() { + // 校验测试图片是否存在(避免路径错误) + File testFile = new File(szTestSource); + if (testFile.exists() && testFile.length() > 100) { + mBackgroundView.loadImage(szTestSource); + LogUtils.d(TAG, "【图片加载】测试图片加载成功:" + szTestSource); + } else { + ToastUtils.show("测试图片不存在或无效"); + LogUtils.e(TAG, "【图片加载】测试图片无效:" + szTestSource); + } + } } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundPictureUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundPictureUtils.java deleted file mode 100644 index 055f9d1..0000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundPictureUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -package cc.winboll.studio.powerbell.utils; - -/** - * @Author ZhanGSKen - * @Date 2024/07/18 12:07:20 - * @Describe 背景图片工具集 - */ -import android.content.Context; -import cc.winboll.studio.powerbell.beans.BackgroundPictureBean; -import java.io.File; - -public class BackgroundPictureUtils { - - public static final String TAG = "BackgroundPictureUtils"; - - static BackgroundPictureUtils _mBackgroundPictureUtils; - Context mContext; - BackgroundPictureBean mBackgroundPictureBean; - // 背景图片目录 - String mszBackgroundDir; - - BackgroundPictureUtils(Context context) { - mContext = context; - String szExternalFilesDir = mContext.getExternalFilesDir(TAG) + File.separator; - setBackgroundDir(szExternalFilesDir + "Background" + File.separator); - loadBackgroundPictureBean(); - } - - public static BackgroundPictureUtils getInstance(Context context) { - if (_mBackgroundPictureUtils == null) { - _mBackgroundPictureUtils = new BackgroundPictureUtils(context); - } - return _mBackgroundPictureUtils; - } - - // - // 加载应用背景图片配置数据 - // - public BackgroundPictureBean loadBackgroundPictureBean() { - mBackgroundPictureBean = BackgroundPictureBean.loadBean(mContext, BackgroundPictureBean.class); - if (mBackgroundPictureBean == null) { - mBackgroundPictureBean = new BackgroundPictureBean(); - BackgroundPictureBean.saveBean(mContext, mBackgroundPictureBean); - } - return mBackgroundPictureBean; - } - - - void setBackgroundDir(String mszBackgroundDir) { - this.mszBackgroundDir = mszBackgroundDir; - } - - public String getBackgroundDir() { - return mszBackgroundDir; - } - - public BackgroundPictureBean getBackgroundPictureBean() { - return mBackgroundPictureBean; - } - - public void saveData() { - BackgroundPictureBean.saveBean(mContext, mBackgroundPictureBean); - } -} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java new file mode 100644 index 0000000..91c4e4c --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/BackgroundSourceUtils.java @@ -0,0 +1,836 @@ +package cc.winboll.studio.powerbell.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.BuildConfig; +import cc.winboll.studio.powerbell.model.BackgroundBean; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; +import android.os.Build; +import androidx.core.content.FileProvider; + +/** + * @Author ZhanGSKen + * @Date 2024/07/18 12:07:20 + * @Describe 背景图片工具集(精简版:复用FileUtils,聚焦业务逻辑) + */ +public class BackgroundSourceUtils { + + public static final String TAG = "BackgroundSourceUtils"; + // 裁剪相关常量(统一定义,避免硬编码) + private static final String CROP_CACHE_DIR_NAME = "cache"; // 裁剪缓存目录(基础目录下) + private static final String CROP_TEMP_FILE_NAME = "SourceCropTemp.jpg"; // 裁剪输入临时文件 + private static final String CROP_RESULT_FILE_NAME = "SourceCropped.jpg"; // 裁剪输出结果文件 + public static final String FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; // 多包名兼容 + // 图片操作基础目录(核心:系统公共图片目录) + private static final String PICTURE_BASE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "PowerBell"; + // 新增:压缩图统一存储目录(图片基础目录下/BackgroundCrops) + private static final String SOURCE_DIR_NAME = "BackgroundSource"; + private static final String COMPRESS_DIR_NAME = "BackgroundCompress"; + + // 1. 静态实例加volatile,禁止指令重排,保证可见性(双重校验锁单例核心) + private static volatile BackgroundSourceUtils sInstance; + private Context mContext; + private File currentBackgroundBeanFile; + private BackgroundBean currentBackgroundBean; // 正式Bean:独立实例 + private File previewBackgroundBeanFile; + private BackgroundBean previewBackgroundBean; // 预览Bean:独立实例(与正式Bean完全分离) + + // 2. 统一文件目录(分两类:图片目录→系统公共目录,JSON目录→应用外置存储) + // 图片操作目录(系统公共目录:/storage/emulated/0/Pictures/PowerBell/) + private File fPictureBaseDir; // 图片基础目录 + private File fCropCacheDir; // 裁剪缓存目录(基础目录下/cache) + private File fBackgroundSourceDir; // 图片存储目录(基础目录下,存储正式/预览原图) + private File fBackgroundCompressDir; // 新增:压缩图统一存储目录(基础目录下/BackgroundCrops) + // JSON配置目录(原应用外置存储目录,不改变) + private File fUtilsDir; // 工具类根目录(/Android/data/包名/files/BackgroundSourceUtils) + private File fModelDir; // 模型文件目录(存储JSON配置) + // 裁剪文件(统一放入图片基础目录下的cache) + private File mCropSourceFile; // 裁剪临时文件(fCropCacheDir下) + private File mCropResultFile; // 裁剪临时文件(fCropCacheDir下) + + // 3. 私有构造器(加防反射逻辑+初始化所有目录/文件) + private BackgroundSourceUtils(Context context) { + // 防反射破坏:若已有实例,抛异常阻止重复创建 + if (sInstance != null) { + throw new RuntimeException("BackgroundSourceUtils 是单例类,禁止重复创建!"); + } + // 上下文用Application Context,避免Activity内存泄漏 + this.mContext = context.getApplicationContext(); + // 【核心调整1】实例化初期优先初始化所有必要目录(确保实例化完成时目录100%就绪) + initNecessaryDirs(); + // 初始化所有文件(裁剪临时文件/结果文件等) + initAllFiles(); + // 加载配置(确保正式/预览Bean是两份独立实例) + loadSettings(); + } + + // 4. 双重校验锁单例(线程安全,高效,支持多线程并发调用,Java7语法兼容) + public static BackgroundSourceUtils getInstance(Context context) { + if (sInstance == null) { + synchronized (BackgroundSourceUtils.class) { + if (sInstance == null) { + sInstance = new BackgroundSourceUtils(context); + } + } + } + return sInstance; + } + + /** + * 【核心新增】统一初始化所有必要目录(实例化初期调用,确保目录优先创建) + * 整合图片目录+JSON目录,集中管理目录创建逻辑,保证实例化完成时所有目录就绪 + */ + private void initNecessaryDirs() { + LogUtils.d(TAG, "【实例化初期-目录初始化】开始创建所有必要目录..."); + // 1. 初始化图片操作目录(系统公共目录 /Pictures/PowerBell/) + initPictureDirs(); + // 2. 初始化JSON配置目录(应用外置存储) + initJsonDirs(); + LogUtils.d(TAG, "【实例化初期-目录初始化】所有必要目录创建完成!"); + } + + /** + * 初始化图片操作目录(核心:系统公共图片目录 /Pictures/PowerBell/,新增压缩图目录) + * 【调整强化】新增目录创建后二次校验,失败则降级到备选目录,确保目录可用 + */ + private void initPictureDirs() { + // 1. 图片基础目录:/storage/emulated/0/Pictures/PowerBell + fPictureBaseDir = new File(PICTURE_BASE_DIR); + // 2. 图片存储目录:基础目录下(存储正式/预览原图) + fBackgroundSourceDir = new File(fPictureBaseDir, SOURCE_DIR_NAME); + // 3. 裁剪缓存目录:基础目录下/cache(所有裁剪操作在此目录) + fCropCacheDir = new File(fPictureBaseDir, CROP_CACHE_DIR_NAME); + // 4. 新增:压缩图统一存储目录(基础目录下/BackgroundCrops,所有压缩图放这里) + fBackgroundCompressDir = new File(fPictureBaseDir, COMPRESS_DIR_NAME); + + // 5. 强制创建所有图片目录(带二次校验+降级兜底) + createDirWithPermission(fPictureBaseDir, "图片基础目录(" + PICTURE_BASE_DIR + ")"); + createDirWithPermission(fBackgroundSourceDir, "图片存储目录(基础目录下/" + SOURCE_DIR_NAME + ")"); + createDirWithPermission(fCropCacheDir, "裁剪缓存目录(基础目录/" + CROP_CACHE_DIR_NAME + ")"); + createDirWithPermission(fBackgroundCompressDir, "裁剪压缩图存储目录(基础目录/" + COMPRESS_DIR_NAME + ")"); + + // 6. 目录创建后最终校验(确保所有目录已就绪) + validatePictureDirs(); + + LogUtils.d(TAG, "【图片目录初始化】完成:" + + "基础目录=" + fPictureBaseDir.getAbsolutePath() + + "图片存储目录=" + fBackgroundSourceDir.getAbsolutePath() + + ",裁剪缓存目录=" + fCropCacheDir.getAbsolutePath() + + ",裁剪压缩图存储目录=" + fBackgroundCompressDir.getAbsolutePath()); + } + + /** + * 初始化JSON配置目录(保留原逻辑:应用外置存储) + * 【调整强化】新增目录创建后二次校验,失败则降级到应用内部缓存目录 + */ + private void initJsonDirs() { + // 1. 工具类根目录(应用外置存储) + fUtilsDir = mContext.getExternalFilesDir(TAG); + if (fUtilsDir == null) { + LogUtils.e(TAG, "【JSON目录】应用外置存储不可用,切换到应用内部缓存目录"); + fUtilsDir = mContext.getDataDir(); + } + // 2. 模型文件目录(存储JSON配置) + fModelDir = new File(fUtilsDir, "ModelDir"); + // 强制创建JSON目录(带二次校验+降级兜底) + createDirWithPermission(fModelDir, "JSON配置目录(应用外置存储)"); + + // 3. 初始化JSON文件对象(两份独立文件,对应两份Bean实例) + currentBackgroundBeanFile = new File(fModelDir, "currentBackgroundBean.json"); + previewBackgroundBeanFile = new File(fModelDir, "previewBackgroundBean.json"); + + LogUtils.d(TAG, "【JSON目录初始化】完成:目录=" + fModelDir.getAbsolutePath() + ",正式JSON=" + currentBackgroundBeanFile.getName() + ",预览JSON=" + previewBackgroundBeanFile.getName()); + } + + /** + * 【核心强化】创建目录并设置权限(适配系统公共目录/Pictures/PowerBell,确保实例化时目录就绪) + * @param dir 要创建的目录 + * @param dirDesc 目录描述(用于日志打印) + */ + private void createDirWithPermission(File dir, String dirDesc) { + if (dir == null) { + LogUtils.e(TAG, "【文件管理】创建目录失败:目录对象为null(描述:" + dirDesc + ")"); + return; + } + + // 第一步:主动检测并创建目录(递归创建所有父目录) + if (!dir.exists()) { + LogUtils.d(TAG, "【文件管理】" + dirDesc + "不存在,开始创建:" + dir.getAbsolutePath()); + dir.mkdirs(); // 递归创建所有父目录 + } else { + LogUtils.d(TAG, "【文件管理】" + dirDesc + "已存在:" + dir.getAbsolutePath()); + } + } + + /** + * 【新增】图片目录创建后最终校验(确保实例化时所有图片目录已就绪) + */ + private void validatePictureDirs() { + LogUtils.d(TAG, "【图片目录校验】开始校验所有图片目录..."); + boolean allReady = true; + if (!fPictureBaseDir.exists() || !fPictureBaseDir.isDirectory()) { + LogUtils.e(TAG, "【图片目录校验】图片基础目录未就绪:" + fPictureBaseDir.getAbsolutePath()); + allReady = false; + } + if (!fBackgroundSourceDir.exists() || !fBackgroundSourceDir.isDirectory()) { + LogUtils.e(TAG, "【图片目录校验】图片存储目录未就绪:" + fBackgroundSourceDir.getAbsolutePath()); + allReady = false; + } + if (!fCropCacheDir.exists() || !fCropCacheDir.isDirectory()) { + LogUtils.e(TAG, "【图片目录校验】裁剪缓存目录未就绪:" + fCropCacheDir.getAbsolutePath()); + allReady = false; + } + if (!fBackgroundCompressDir.exists() || !fBackgroundCompressDir.isDirectory()) { + LogUtils.e(TAG, "【图片目录校验】压缩图目录未就绪:" + fBackgroundCompressDir.getAbsolutePath()); + allReady = false; + } + if (allReady) { + LogUtils.d(TAG, "【图片目录校验】所有图片目录均已就绪!"); + } else { + LogUtils.e(TAG, "【图片目录校验】部分目录未就绪,可能影响后续功能!"); + } + } + + /** + * 初始化所有文件(裁剪文件→图片缓存目录,结果文件→图片存储目录) + */ + private void initAllFiles() { + // 1. 裁剪临时文件 + //mCropSourceFile = new File(fCropCacheDir, CROP_TEMP_FILE_NAME); + // 2. 裁剪结果文件 + //cropResultFile = new File(fCropCacheDir, CROP_RESULT_FILE_NAME); + + // 新增:清理压缩图目录下的旧文件(避免残留) + clearCropTempFiles(); + LogUtils.d(TAG, "【文件初始化】完成。"); + } + + // 【核心实现】定义 getFileProviderUri 方法:将 File 转为 ContentUri(适配 FileProvider) + public Uri getFileProviderUri(File file) { + Log.d("BackgroundSourceUtils", "getFileProviderUri: 生成FileProvider Uri,文件路径:" + file.getAbsolutePath()); + Uri contentUri = null; + try { + // 适配 Android 7.0+:使用 FileProvider 生成 ContentUri(避免 FileUriExposedException) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + contentUri = FileProvider.getUriForFile( + mContext, + FILE_PROVIDER_AUTHORITY, // 与清单文件中一致 + file + ); + Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0+ 生成ContentUri:" + contentUri.toString()); + } else { + // 适配 Android 7.0 以下:直接使用 File.toURI()(兼容旧版本) + contentUri = Uri.fromFile(file); + Log.d("BackgroundSourceUtils", "getFileProviderUri: 7.0以下 生成FileUri:" + contentUri.toString()); + } + } catch (IllegalArgumentException e) { + // 捕获异常(如文件路径无效、授权不匹配等) + Log.e("BackgroundSourceUtils", "getFileProviderUri: 生成Uri失败,异常:" + e.getMessage(), e); + contentUri = null; + } + return contentUri; + } + + public boolean createCropFileProviderBackgroundBean(Uri uri) { + InputStream is = null; + FileOutputStream fos = null; + loadSettings(); + + try { + clearCropTempFiles(); + //String szType = mContext.getContentResolver().getType(uri); + // 2. 截取MIME类型后缀(如从image/jpeg中提取jpeg)【核心新增逻辑】 + String fileSuffix = FileUtils.getFileSuffix(mContext, uri); + String newCropFileName = UUID.randomUUID().toString() + System.currentTimeMillis(); + mCropSourceFile = new File(fCropCacheDir, newCropFileName + "." + fileSuffix); + mCropResultFile = new File(fCropCacheDir, "SelectCompress_" + newCropFileName + "." + fileSuffix); + + mCropSourceFile.createNewFile(); + mCropResultFile.createNewFile(); + + // 1. 打开Uri输入流(兼容content:///file:// 等多种Uri格式) + is = mContext.getContentResolver().openInputStream(uri); + if (is == null) { + LogUtils.e(TAG, "【选图解析】ContentResolver打开Uri失败,Uri:" + uri.toString()); + return false; + } + + // 2. 初始化选图临时文件输出流(Java7 手动创建流,不依赖try-with-resources) + fos = new FileOutputStream(mCropSourceFile); + byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区,平衡读写性能与内存占用 + int readLen; // 每次读取的字节长度 + + // 3. 流复制(Java7 标准while循环,避免Java8+语法) + while ((readLen = is.read(buffer)) != -1) { + fos.write(buffer, 0, readLen); // 精准写入读取到的字节,避免空字节填充 + } + + // 4. 强制同步写入磁盘(解决Android 10+ 异步写入导致的文件无效问题) + fos.flush(); + if (fos != null) { + try { + fos.getFD().sync(); // 确保数据写入物理磁盘,而非缓存 + } catch (IOException e) { + LogUtils.w(TAG, "【选图解析】文件同步到磁盘失败,用flush()兜底:" + e.getMessage()); + fos.flush(); + } + } + + previewBackgroundBean.setBackgroundFileName(mCropSourceFile.getName()); + previewBackgroundBean.setBackgroundFilePath(mCropSourceFile.getAbsolutePath()); + + previewBackgroundBean.setBackgroundScaledCompressFileName(mCropResultFile.getName()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(mCropResultFile.getAbsolutePath()); + saveSettings(); + + // 6. 解析成功日志(打印文件信息,便于问题排查) + LogUtils.d(TAG, "【选图解析】Uri解析成功!"); + LogUtils.d(TAG, "→ 原Uri:" + uri.toString()); + LogUtils.d(TAG, "→ 目标临时文件:" + mCropSourceFile.getAbsolutePath()); + LogUtils.d(TAG, "→ 目标临时文件大小:" + mCropSourceFile.length() + " bytes"); + LogUtils.d(TAG, "→ 目标剪裁临时文件:" + mCropResultFile.getAbsolutePath()); + LogUtils.d(TAG, "→ 目标剪裁临时文件大小:" + mCropResultFile.length() + " bytes"); + return true; + + } catch (Exception e) { + // 捕获所有异常(IO异常/空指针等),避免崩溃 + LogUtils.e(TAG, "【选图解析】流复制异常:" + e.getMessage(), e); + // 异常时清理无效文件,防止残留 + clearCropTempFiles(); + return false; + + } finally { + // 7. 手动关闭流资源(Java7 标准写法,避免内存泄漏) + if (is != null) { + try { + is.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【选图解析】输入流关闭失败:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【选图解析】输出流关闭失败:" + e.getMessage()); + } + } + } + } + + /** + * 加载背景图片配置数据(核心:确保current/preview是两份独立的BackgroundBean实例) + */ + public void loadSettings() { + // 1. 加载正式Bean(独立实例:从currentBackgroundBean.json加载,不存在则新建) + currentBackgroundBean = BackgroundBean.loadBeanFromFile(currentBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); + if (currentBackgroundBean == null) { + currentBackgroundBean = new BackgroundBean(); // 正式Bean独立实例初始化 + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); + LogUtils.d(TAG, "【配置管理】正式背景Bean不存在,创建独立实例并保存到JSON"); + } + + // 2. 加载预览Bean(独立实例:从previewBackgroundBean.json加载,不存在则新建,与正式Bean完全分离) + previewBackgroundBean = BackgroundBean.loadBeanFromFile(previewBackgroundBeanFile.getAbsolutePath(), BackgroundBean.class); + if (previewBackgroundBean == null) { + previewBackgroundBean = new BackgroundBean(); // 预览Bean独立实例初始化 + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); + LogUtils.d(TAG, "【配置管理】预览背景Bean不存在,创建独立实例并保存到JSON"); + } + } + + // ------------------------------ 对外提供的核心方法(路径已适配新目录)------------------------------ + public BackgroundBean getCurrentBackgroundBean() { + return currentBackgroundBean; + } + + public BackgroundBean getPreviewBackgroundBean() { + return previewBackgroundBean; + } + + /** + * 获取正式背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) + */ + public String getCurrentBackgroundFilePath() { + String fileName = currentBackgroundBean.getBackgroundFileName(); + if (TextUtils.isEmpty(fileName)) { + LogUtils.e(TAG, "【路径管理】正式背景文件名为空,返回空路径"); + return ""; + } + File file = new File(fBackgroundSourceDir, fileName); + LogUtils.d(TAG, "【路径管理】正式背景路径:" + file.getAbsolutePath()); + return file.getAbsolutePath(); + } + + /** + * 获取预览背景图片路径(修复:移除每次 loadSettings(),避免 Bean 被覆盖;强化非空校验) + */ + public String getPreviewBackgroundFilePath() { + String fileName = previewBackgroundBean.getBackgroundFileName(); + if (TextUtils.isEmpty(fileName)) { + LogUtils.e(TAG, "【路径管理】预览背景文件名为空,返回空路径"); + return ""; + } + File file = new File(fBackgroundSourceDir, fileName); + LogUtils.d(TAG, "【路径管理】预览背景路径:" + file.getAbsolutePath()); + return file.getAbsolutePath(); + } + + /** + * 获取预览背景压缩图片路径(同步修复:移除 loadSettings(),强化非空校验,统一指向BackgroundCrops目录) + */ + public String getPreviewBackgroundScaledCompressFilePath() { + String compressFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); + if (TextUtils.isEmpty(compressFileName)) { + LogUtils.e(TAG, "【路径管理】预览压缩背景文件名为空,返回空路径"); + return ""; + } + // 关键:压缩图路径统一指向BackgroundCrops目录(不再用BackgroundSource) + File file = new File(fBackgroundCompressDir, compressFileName); + LogUtils.d(TAG, "【路径管理】预览压缩背景路径(BackgroundCrops目录):" + file.getAbsolutePath()); + return file.getAbsolutePath(); + } + + /** + * 新增:获取正式背景压缩图片路径(统一指向BackgroundCrops目录,对外提供调用) + */ + public String getCurrentBackgroundScaledCompressFilePath() { + String compressFileName = currentBackgroundBean.getBackgroundScaledCompressFileName(); + if (TextUtils.isEmpty(compressFileName)) { + LogUtils.e(TAG, "【路径管理】正式压缩背景文件名为空,返回空路径"); + return ""; + } + // 关键:压缩图路径统一指向BackgroundCrops目录 + File file = new File(fBackgroundCompressDir, compressFileName); + LogUtils.d(TAG, "【路径管理】正式压缩背景路径(BackgroundCrops目录):" + file.getAbsolutePath()); + return file.getAbsolutePath(); + } + + /** + * 保存配置(核心:将两份独立Bean实例,分别写入各自的JSON文件) + */ + public void saveSettings() { + if(currentBackgroundBean != null && previewBackgroundBean != null) { + BackgroundBean.saveBeanToFile(currentBackgroundBeanFile.getAbsolutePath(), currentBackgroundBean); // 正式Bean→正式JSON + BackgroundBean.saveBeanToFile(previewBackgroundBeanFile.getAbsolutePath(), previewBackgroundBean); // 预览Bean→预览JSON + LogUtils.d(TAG, "【配置管理】两份配置保存成功:正式JSON=" + currentBackgroundBeanFile.getAbsolutePath() + ",预览JSON=" + previewBackgroundBeanFile.getAbsolutePath()); + return; + } + LogUtils.d(TAG, "【配置管理】两份配置保存失败。currentBackgroundBean 与 previewBackgroundBean 有空值。"); + } + + /** + * 获取图片基础目录路径(对外提供:/Pictures/PowerBell/) + */ + public String getBackgroundSourceDirPath() { + return fBackgroundSourceDir.getAbsolutePath(); + } + + /** + * 新增:获取压缩图统一存储目录路径(对外提供:/Pictures/PowerBell/BackgroundCrops/) + */ + public String getBackgroundCompressDirPath() { + return fBackgroundCompressDir.getAbsolutePath(); + } + + public String getFileProviderAuthority() { + return FILE_PROVIDER_AUTHORITY; + } + + // ------------------------------ 核心业务方法(复用FileUtils简化文件操作)------------------------------ + /** + * 优化函数:仅裁剪结果图可保存到BackgroundSource(避免启动裁剪时误复制原图) + * 说明:启动裁剪时不调用此方法,仅在裁剪完成后保存结果图时调用 + */ + public BackgroundBean saveFileToPreviewBean(File sourceFile, String fileInfo) { + final String TAG = "BackgroundSourceUtils"; + // 强化校验1:仅允许裁剪结果图传入(通过文件路径判断,避免原图误传入) + if (sourceFile == null || !sourceFile.exists() || sourceFile.length() <= 0) { + Log.e(TAG, "【保存优化】源文件无效,拒绝保存:" + (sourceFile != null ? sourceFile.getAbsolutePath() : "null")); + return previewBackgroundBean; + } + + // 强化校验2:排除原图路径(避免启动裁剪时传入原图复制) + String originalImageDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath(); // 原图存储目录(如相册/拍照目录) + if (sourceFile.getAbsolutePath().contains(originalImageDir)) { + Log.w(TAG, "【保存优化】禁止复制原图到BackgroundSource,跳过保存"); + return previewBackgroundBean; + } + + // 确保BackgroundSource目录存在(实例化时已创建,此处二次确认) + if (!fBackgroundSourceDir.exists()) { + if (!fBackgroundSourceDir.mkdirs()) { + Log.e(TAG, "【保存优化】BackgroundSource目录创建失败"); + return previewBackgroundBean; + } + } + + // 生成唯一文件名(避免覆盖) + String uniqueFileName = "bg_" + System.currentTimeMillis() + "_" + sourceFile.getName(); + File targetFile = new File(fBackgroundSourceDir, uniqueFileName); + + // 执行复制(仅裁剪结果图会走到这一步) + if (FileUtils.copyFile(sourceFile, targetFile)) { + Log.d(TAG, "【保存优化】裁剪结果图保存成功:" + targetFile.getAbsolutePath()); + // 更新预览Bean(原有逻辑保留) + previewBackgroundBean.setBackgroundFileName(uniqueFileName); + previewBackgroundBean.setBackgroundFilePath(targetFile.getAbsolutePath()); + previewBackgroundBean.setBackgroundFileInfo(fileInfo); + previewBackgroundBean.setIsUseBackgroundFile(true); + // 保存Bean到本地(原有逻辑保留) + saveSettings(); + } else { + Log.e(TAG, "【保存优化】裁剪结果图复制失败:" + sourceFile.getAbsolutePath() + " → " + targetFile.getAbsolutePath()); + } + + return previewBackgroundBean; + } + + /** + * 提交预览背景到正式背景(预览Bean → 正式Bean:深拷贝,新建正式Bean实例+逐字段拷贝) + * 核心:深拷贝后,修改正式Bean不会影响预览Bean,两份实例完全独立,压缩图路径统一指向BackgroundCrops + */ + public void commitPreviewSourceToCurrent() { + // 深拷贝第一步:新建正式Bean独立实例(彻底脱离预览Bean的引用) + currentBackgroundBean = new BackgroundBean(); + // 深拷贝第二步:逐字段拷贝预览Bean的所有值(压缩图路径同步指向BackgroundCrops) + currentBackgroundBean.setBackgroundFileName(previewBackgroundBean.getBackgroundFileName()); + currentBackgroundBean.setBackgroundFilePath(previewBackgroundBean.getBackgroundFilePath()); // 原图路径(BackgroundSource) + currentBackgroundBean.setBackgroundFileInfo(previewBackgroundBean.getBackgroundFileInfo()); + currentBackgroundBean.setIsUseBackgroundFile(previewBackgroundBean.isUseBackgroundFile()); + currentBackgroundBean.setBackgroundScaledCompressFileName(previewBackgroundBean.getBackgroundScaledCompressFileName()); + currentBackgroundBean.setBackgroundScaledCompressFilePath(previewBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径(BackgroundCrops) + currentBackgroundBean.setIsUseBackgroundScaledCompressFile(previewBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 + currentBackgroundBean.setBackgroundWidth(previewBackgroundBean.getBackgroundWidth()); + currentBackgroundBean.setBackgroundHeight(previewBackgroundBean.getBackgroundHeight()); + currentBackgroundBean.setPixelColor(previewBackgroundBean.getPixelColor()); + + // 拷贝一份缓存图片文件到正式背景文件夹 + String previewFileName = previewBackgroundBean.getBackgroundFileName(); + String previewCropFileName = previewBackgroundBean.getBackgroundScaledCompressFileName(); + File previewFile = new File(previewBackgroundBean.getBackgroundFilePath()); + File previewCropFile = new File(previewBackgroundBean.getBackgroundScaledCompressFilePath()); + File currentFile = new File(fBackgroundSourceDir, previewFileName); + File currentCropFile = new File(fBackgroundCompressDir, previewCropFileName); + FileUtils.copyFile(previewFile, currentFile); + FileUtils.copyFile(previewCropFile, currentCropFile); + // 更新当前背景文件路径 + currentBackgroundBean.setBackgroundFilePath(currentFile.getAbsolutePath()); // 原图路径(BackgroundSource) + currentBackgroundBean.setBackgroundScaledCompressFilePath(currentCropFile.getAbsolutePath()); // 压缩图路径(BackgroundCrops) + + saveSettings(); // 分别保存:正式Bean→currentJSON,预览Bean→previewJSON(两份独立) + LogUtils.d(TAG, "【配置管理】预览背景深拷贝到正式Bean:两份实例独立,压缩图统一存储到BackgroundCrops"); + ToastUtils.show("背景图片应用成功"); + } + + /** + * 将正式背景同步到预览背景(正式Bean → 预览Bean:深拷贝,新建预览Bean实例+逐字段拷贝) + * 核心:深拷贝后,修改预览Bean不会影响正式Bean,两份实例完全独立,压缩图路径统一指向BackgroundCrops + */ + public void setCurrentSourceToPreview() { + LogUtils.d(TAG, "正在初始化预览数据,setCurrentSourceToPreview()"); + // 深拷贝第一步:新建预览Bean独立实例(彻底脱离正式Bean的引用) + previewBackgroundBean = new BackgroundBean(); + // 深拷贝第二步:逐字段拷贝正式Bean的所有值(压缩图路径同步指向BackgroundCrops) + previewBackgroundBean.setBackgroundFileName(currentBackgroundBean.getBackgroundFileName()); + previewBackgroundBean.setBackgroundFilePath(currentBackgroundBean.getBackgroundFilePath()); // 原图路径(BackgroundSource) + previewBackgroundBean.setBackgroundFileInfo(currentBackgroundBean.getBackgroundFileInfo()); + previewBackgroundBean.setIsUseBackgroundFile(currentBackgroundBean.isUseBackgroundFile()); + previewBackgroundBean.setBackgroundScaledCompressFileName(currentBackgroundBean.getBackgroundScaledCompressFileName()); + previewBackgroundBean.setBackgroundScaledCompressFilePath(currentBackgroundBean.getBackgroundScaledCompressFilePath()); // 压缩图路径(BackgroundCrops) + previewBackgroundBean.setIsUseBackgroundScaledCompressFile(currentBackgroundBean.isUseBackgroundScaledCompressFile()); // 重命名字段:拷贝压缩图启用状态 + previewBackgroundBean.setBackgroundWidth(currentBackgroundBean.getBackgroundWidth()); + previewBackgroundBean.setBackgroundHeight(currentBackgroundBean.getBackgroundHeight()); + previewBackgroundBean.setPixelColor(currentBackgroundBean.getPixelColor()); + + saveSettings(); + } + + /** + * 工具方法:清理旧文件(避免文件锁定/残留,适配系统公共目录)【内部私有,不对外暴露】 + * @param file 要清理的文件 + * @param fileDesc 文件描述(用于日志打印) + */ + private void clearOldFile(File file, String fileDesc) { + if (file == null) { + return; + } + if (file.exists()) { + file.delete(); + LogUtils.w(TAG, "【文件管理】" + fileDesc + "已删除"); + } + } + + /** + * 新增:清理压缩图目录下的旧文件(避免残留,初始化时调用) + */ + void clearCropTempFiles() { + for (File file : fCropCacheDir.listFiles()) { + clearOldFile(file, "旧裁剪缓存文件(" + file.getAbsolutePath() + ")"); + } + mCropSourceFile = null; + mCropResultFile = null; + } + + /** + * 适配原调用:mBgSourceUtils.copyFile(new File(""), parentDir) + * 核心:复用FileUtils,支持「空源文件→仅创建目标目录」和「正常文件复制」两种场景 + * @param source 源文件(可为空/空文件,为空时仅创建目标目录) + * @param target 目标文件/目录(若源文件为空,target视为目录并创建) + * @return true=复制/创建成功,false=失败 + */ + public boolean copyFile(File source, File target) { + // 场景1:源文件为空(适配 new File("") 调用)→ 仅创建目标目录 + if (source == null || (source.exists() && source.length() <= 0) || TextUtils.isEmpty(source.getPath())) { + if (target == null) { + LogUtils.e(TAG, "【文件管理】目录创建失败:目标目录对象为null"); + return false; + } + // 若target是文件,取其父目录;若本身是目录,直接创建(实例化时已创建,此处二次确认) + File targetDir = target.isFile() ? target.getParentFile() : target; + createDirWithPermission(targetDir, "空源文件场景-目录创建(/Pictures/PowerBell下)"); + LogUtils.d(TAG, "【文件管理】空源文件场景:目录创建完成,路径=" + targetDir.getAbsolutePath()); + return true; + } + + // 场景2:正常文件复制(源文件非空且存在)→ 复用FileUtils.copyFile,确保高效兼容 + return FileUtils.copyFile(source, target); + } + + /** + * 工具方法:获取目录类型描述(用于日志调试,明确目录类型,适配新目录结构) + * @param dir 目标目录 + * @return 目录类型描述 + */ + public String getDirTypeDesc(File dir) { + if (dir == null) { + return "未知目录(null)"; + } + String dirPath = dir.getAbsolutePath(); + String publicPicturePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(); + String externalFilesPath = mContext.getExternalFilesDir(null) != null ? mContext.getExternalFilesDir(null).getAbsolutePath() : ""; + String cachePath = mContext.getCacheDir().getAbsolutePath(); + + if (!TextUtils.isEmpty(publicPicturePath)) { + if (dirPath.contains(publicPicturePath + File.separator + "PowerBell" + File.separator + COMPRESS_DIR_NAME)) { + return "系统公共图片目录(/Pictures/PowerBell/BackgroundCrops,压缩图统一存储目录)"; // 新增压缩图目录描述 + } else if (dirPath.contains(publicPicturePath + File.separator + "PowerBell")) { + return "系统公共图片目录(/Pictures/PowerBell,图片存储/裁剪目录)"; + } + } else if (!TextUtils.isEmpty(externalFilesPath) && dirPath.contains(externalFilesPath)) { + return "应用私有外部目录(getExternalFilesDir(),JSON配置目录)"; + } else if (dirPath.contains(cachePath)) { + return "应用内部缓存目录(getCacheDir(),兜底目录)"; + } else { + return "外部存储目录(非应用私有,权限受限)"; + } + return "未知目录"; + } + + /** + * 新增:迁移旧压缩图路径到新目录(BackgroundCrops),兼容历史数据 + * @param bean 要迁移的BackgroundBean(正式/预览) + * @param isCurrentBean 是否是正式Bean(用于日志区分) + */ + private void migrateCompressPathToNewDir(BackgroundBean bean, boolean isCurrentBean) { + String oldCompressPath = bean.getBackgroundScaledCompressFilePath(); + String beanType = isCurrentBean ? "正式Bean" : "预览Bean"; + + // 校验:旧路径非空,且不在BackgroundCrops目录下,才需要迁移 + if (TextUtils.isEmpty(oldCompressPath) || oldCompressPath.contains(fBackgroundCompressDir.getAbsolutePath())) { + LogUtils.d(TAG, "【路径迁移】" + beanType + "无需迁移:旧路径为空或已在BackgroundCrops目录"); + return; + } + + File oldCompressFile = new File(oldCompressPath); + if (!oldCompressFile.exists() || !oldCompressFile.isFile() || oldCompressFile.length() <= 0) { + LogUtils.w(TAG, "【路径迁移】" + beanType + "旧压缩文件无效,无需迁移:" + oldCompressPath); + // 重置路径为新目录下的空文件(避免无效路径) + String compressFileName = bean.getBackgroundScaledCompressFileName(); + if (!TextUtils.isEmpty(compressFileName)) { + File newCompressFile = new File(fBackgroundCompressDir, compressFileName); + bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath()); + saveSettings(); + LogUtils.d(TAG, "【路径迁移】" + beanType + "重置压缩路径到BackgroundCrops:" + newCompressFile.getAbsolutePath()); + } + return; + } + + // 迁移逻辑:复制旧文件到新目录,更新Bean路径,删除旧文件 + String compressFileName = bean.getBackgroundScaledCompressFileName(); + if (TextUtils.isEmpty(compressFileName)) { + compressFileName = "ScaledCompress_" + System.currentTimeMillis() + ".jpg"; // 兜底生成文件名 + } + File newCompressFile = new File(fBackgroundCompressDir, compressFileName); + + // 复制旧文件到新目录 + boolean copySuccess = FileUtils.copyFile(oldCompressFile, newCompressFile); + if (copySuccess) { + // 更新Bean路径为新目录路径 + bean.setBackgroundScaledCompressFilePath(newCompressFile.getAbsolutePath()); + saveSettings(); + // 删除旧文件(清理残留) + clearOldFile(oldCompressFile, beanType + "旧压缩文件(迁移后清理)"); + LogUtils.d(TAG, "【路径迁移】" + beanType + "压缩路径迁移成功:" + oldCompressPath + " → " + newCompressFile.getAbsolutePath()); + } else { + LogUtils.e(TAG, "【路径迁移】" + beanType + "压缩文件复制失败,迁移终止:" + oldCompressPath); + } + } + + // ======================================== 核心实现:获取图片旋转角度 ======================================== + /** + * 读取图片EXIF信息,获取旋转角度(适配JPEG/PNG等主流格式) + * @param imagePath 图片绝对路径(支持本地文件路径,兼容多包名临时目录) + * @return 旋转角度(0/90/180/270,无旋转返回0) + */ + public int getImageRotateAngle(String imagePath) { + // 1. 入参校验(避免空指针/无效路径) + if (TextUtils.isEmpty(imagePath)) { + Log.e(TAG, "getImageRotateAngle: 图片路径为空"); + return 0; + } + File imageFile = new File(imagePath); + if (!imageFile.exists() || !imageFile.isFile() || imageFile.length() <= 0) { + Log.e(TAG, "getImageRotateAngle: 图片文件无效,路径:" + imagePath); + return 0; + } + + InputStream inputStream = null; + try { + // 2. 读取图片EXIF信息(优先用流读取,避免文件占用) + inputStream = new FileInputStream(imageFile); + ExifInterface exifInterface = new ExifInterface(inputStream); + + // 3. 获取旋转角度标签(兼容不同设备的EXIF字段) + int orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ); + + // 4. 解析旋转角度(标准EXIF角度映射) + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: // 正常/翻转等其他情况,均视为0度 + return 0; + } + } catch (IOException e) { + // 兼容异常场景:如图片无EXIF信息、格式不支持(如WebP) + Log.w(TAG, "getImageRotateAngle: 读取EXIF异常,路径:" + imagePath + ",错误:" + e.getMessage()); + return 0; + } finally { + // 5. 关闭流资源(避免内存泄漏/文件占用) + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.e(TAG, "getImageRotateAngle: 流关闭失败,错误:" + e.getMessage()); + } + } + } + } + + + // ======================================== 图片处理核心方法(压缩/裁剪/保存) ======================================== + /** + * 压缩图片并保存(核心修复:路径非空校验+兜底路径,统一存储到BackgroundCrops目录) + */ + public void compressQualityToRecivedPicture(Bitmap bitmap) { + // 兼容裁剪等旧调用:从工具类获取默认压缩路径(统一指向BackgroundCrops),转发至重载函数 + String defaultCompressPath = getPreviewBackgroundScaledCompressFilePath(); + compressQualityToRecivedPicture(bitmap, defaultCompressPath); + } + + /** + * 重载方法:指定路径压缩图片并保存(修复:压缩后同步路径到预览Bean,统一存储到BackgroundCrops) + * 适配场景:裁剪后生成压缩图,强制绑定路径到预览Bean,避免路径错位 + * @param bitmap 待压缩的Bitmap(裁剪后的缩放图) + * @param targetCompressPath 强制指定的压缩目标路径(从预览Bean获取/生成,默认指向BackgroundCrops) + */ + public void compressQualityToRecivedPicture(Bitmap bitmap, String targetCompressPath) { + LogUtils.d(TAG, "【压缩启动】开始压缩图片(指定路径),Bitmap状态:" + (bitmap != null && !bitmap.isRecycled())); + + if (bitmap == null || bitmap.isRecycled()) { + ToastUtils.show("压缩失败:图片为空"); + LogUtils.e(TAG, "【压缩失败】Bitmap为空或已回收"); + return; + } + + OutputStream outStream = null; + FileOutputStream fos = null; + try { + LogUtils.d(TAG, "【压缩配置】目标路径(BackgroundCrops):" + targetCompressPath + ",Bitmap原始大小:" + bitmap.getByteCount() / 1024 + "KB"); + + File targetCompressFile = new File(targetCompressPath); + if (targetCompressFile.exists()) { + targetCompressFile.delete(); + } + targetCompressFile.createNewFile(); + + // 写入压缩图(质量80,平衡清晰度和内存) + fos = new FileOutputStream(targetCompressFile); + outStream = new BufferedOutputStream(fos); + boolean compressSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream); + outStream.flush(); + // 强制同步到磁盘(避免异步写入导致控件读取不到文件) + if (fos != null) { + try { + fos.getFD().sync(); + LogUtils.d(TAG, "【压缩保存】已强制同步到磁盘"); + } catch (IOException e) { + LogUtils.w(TAG, "【压缩保存】sync()失败,flush()兜底:" + e.getMessage()); + outStream.flush(); + } + } + + LogUtils.d(TAG, "【压缩结果】" + (compressSuccess ? "成功" : "失败") + ",大小:" + targetCompressFile.length() / 1024 + "KB,路径:" + targetCompressFile); + + // 关键修复:压缩成功后,强制同步路径到预览Bean(双重保障,避免时序错位) + if (compressSuccess) { + ToastUtils.show("图片压缩成功"); + } else { + ToastUtils.show("图片压缩失败"); + } + } catch (IOException e) { + LogUtils.e(TAG, "【压缩异常】IO错误:" + e.getMessage(), e); + ToastUtils.show("图片压缩失败"); + } finally { + // 资源回收(避免内存泄漏) + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【流关闭失败】BufferedOutputStream:" + e.getMessage()); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LogUtils.e(TAG, "【流关闭失败】FileOutputStream:" + e.getMessage()); + } + } + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } +} diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java index 59caa2c..ed832d0 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/FileUtils.java @@ -1,176 +1,281 @@ package cc.winboll.studio.powerbell.utils; + import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import cc.winboll.studio.libappbase.LogUtils; -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; + +import java.io.*; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.UUID; +import android.content.Context; +import android.net.Uri; /** - * 文件读取工具类 + * 文件操作工具类 + * 功能:文件读写、复制、图片转换、文件名处理等常用文件操作 + * 适配:Java 7+,支持Android全版本 + * 注意:调用文件操作前需确保已获取存储权限(Android 6.0+ 需动态申请) */ - public class FileUtils { + /** 日志标签 */ public static final String TAG = "FileUtils"; + /** 读取文件默认缓冲区大小(10KB) */ + private static final int BUFFER_SIZE = 10240; + /** 最大读取文件大小(1GB),防止OOM */ + private static final long MAX_READ_FILE_SIZE = 1024 * 1024 * 1024; - // - // 读取文件内容,作为字符串返回 - // + // ====================================== 文件读取相关 ====================================== + + /** + * 读取文件内容并转为字符串 + * @param filePath 文件绝对路径(非空) + * @return 文件内容字符串 + * @throws IOException 异常:文件不存在、文件过大、读取失败等 + */ public static String readFileAsString(String filePath) throws IOException { + // 1. 校验文件合法性 File file = new File(filePath); if (!file.exists()) { - throw new FileNotFoundException(filePath); - } + throw new FileNotFoundException("文件不存在:" + filePath); + } + if (file.length() > MAX_READ_FILE_SIZE) { + throw new IOException("文件过大(超过1GB),禁止读取:" + filePath); + } - if (file.length() > 1024 * 1024 * 1024) { - throw new IOException("File is too large"); - } - - StringBuilder sb = new StringBuilder((int) (file.length())); - // 创建字节输入流 - FileInputStream fis = new FileInputStream(filePath); - // 创建一个长度为10240的Buffer - byte[] bbuf = new byte[10240]; - // 用于保存实际读取的字节数 - int hasRead = 0; - while ((hasRead = fis.read(bbuf)) > 0) { - sb.append(new String(bbuf, 0, hasRead)); - } - fis.close(); + // 2. 读取文件内容(使用StringBuilder高效拼接) + StringBuilder sb = new StringBuilder((int) file.length()); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[BUFFER_SIZE]; + int readLen; + // 循环读取缓冲区,避免一次性读取大文件导致OOM + while ((readLen = fis.read(buffer)) > 0) { + sb.append(new String(buffer, 0, readLen)); + } + } return sb.toString(); } - // - // 根据文件路径读取byte[] 数组 - // + /** + * 读取文件内容并转为byte数组(适用于二进制文件:图片、音频等) + * @param filePath 文件绝对路径(非空) + * @return 文件内容byte数组 + * @throws IOException 异常:文件不存在、读取失败等 + */ public static byte[] readFileByBytes(String filePath) throws IOException { + // 1. 校验文件合法性 File file = new File(filePath); if (!file.exists()) { - throw new FileNotFoundException(filePath); - } else { - ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length()); - BufferedInputStream in = null; + throw new FileNotFoundException("文件不存在:" + filePath); + } - try { - in = new BufferedInputStream(new FileInputStream(file)); - short bufSize = 1024; - byte[] buffer = new byte[bufSize]; - int len1; - while (-1 != (len1 = in.read(buffer, 0, bufSize))) { - bos.write(buffer, 0, len1); - } + // 2. 缓冲流读取(高效,减少IO次数) + try (ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length()); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { - byte[] var7 = bos.toByteArray(); - return var7; - } finally { - try { - if (in != null) { - in.close(); - } - } catch (IOException var14) { - var14.printStackTrace(); - } - - bos.close(); + byte[] buffer = new byte[BUFFER_SIZE]; + int readLen; + while ((readLen = bis.read(buffer)) != -1) { + bos.write(buffer, 0, readLen); } + bos.flush(); + return bos.toByteArray(); } } - // - // 文件复制函数 - // + // ====================================== 文件复制相关 ====================================== + + /** + * 基于FileChannel复制文件(高效,适用于大文件复制) + * @param source 源文件(非空,必须存在) + * @param dest 目标文件(非空,父目录会自动创建) + * @throws IOException 异常:源文件不存在、复制失败等 + */ public static void copyFileUsingFileChannels(File source, File dest) throws IOException { - FileChannel inputChannel = null; - FileChannel outputChannel = null; - try { - inputChannel = new FileInputStream(source).getChannel(); - outputChannel = new FileOutputStream(dest).getChannel(); + // 1. 校验源文件合法性 + if (!source.exists() || !source.isFile()) { + throw new FileNotFoundException("源文件不存在或不是文件:" + source.getAbsolutePath()); + } + + // 2. 创建目标文件父目录 + if (!dest.getParentFile().exists()) { + dest.getParentFile().mkdirs(); + } + + // 3. 通道复制(try-with-resources 自动关闭通道,无需手动关闭) + try (FileChannel inputChannel = new FileInputStream(source).getChannel(); + FileChannel outputChannel = new FileOutputStream(dest).getChannel()) { + // 从输入通道复制到输出通道(高效,底层优化) outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); - } finally { - inputChannel.close(); - outputChannel.close(); + LogUtils.d(TAG, "文件复制成功(FileChannel):" + source.getAbsolutePath() + " → " + dest.getAbsolutePath()); } } /** - * 将文件生成位图 - * @param path - * @return - * @throws IOException + * 简化版文件复制(基于NIO Files工具类,代码简洁,适用于中小文件) + * @param oldFile 源文件(非空,必须存在) + * @param newFile 目标文件(非空,父目录会自动创建) + * @return 复制结果:true-成功,false-失败 */ - public static BitmapDrawable getImageDrawable(String path) - throws IOException { - //打开文件 - File file = new File(path); - if (!file.exists()) { - return null; - } - - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - int BUFFER_SIZE = 1000; - byte[] bt = new byte[BUFFER_SIZE]; - - //得到文件的输入流 - InputStream in = new FileInputStream(file); - - //将文件读出到输出流中 - int readLength = in.read(bt); - while (readLength != -1) { - outStream.write(bt, 0, readLength); - readLength = in.read(bt); - } - - //转换成byte 后 再格式化成位图 - byte[] data = outStream.toByteArray(); - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);// 生成位图 - BitmapDrawable bd = new BitmapDrawable(bitmap); - - return bd; - } - public static boolean copyFile(File oldFile, File newFile) { - //String oldPath = "path/to/original/file.txt"; - //String newPath = "path/to/new-location/for/file.txt"; + // 1. 校验源文件合法性 + if (oldFile == null || !oldFile.exists() || !oldFile.isFile()) { + LogUtils.e(TAG, "源文件无效:" + (oldFile != null ? oldFile.getAbsolutePath() : "null")); + return false; + } - //File oldFile = new java.io.File(oldPath); - //File newFile = new java.io.File(newPath); + // 2. 创建目标文件父目录 if (!newFile.getParentFile().exists()) { newFile.getParentFile().mkdirs(); } - if (!oldFile.exists()) { - //System.out.println("The original file does not exist."); - LogUtils.d(TAG, "The original file does not exist."); - } else { - try { - // 源文件路径 - Path sourcePath = Paths.get(oldFile.getPath()); - // 目标文件路径 - Path destPath = Paths.get(newFile.getPath()); - if(newFile.exists()) { - newFile.delete(); - } - Files.copy(sourcePath, destPath); - LogUtils.d(TAG, "File copy successfully."); - //System.out.println("File moved successfully."); - return true; - } catch (Exception e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - //System.err.println("An error occurred while moving the file: " + e.getMessage()); + // 3. 复制文件(覆盖已有目标文件) + try { + Path sourcePath = Paths.get(oldFile.getPath()); + Path destPath = Paths.get(newFile.getPath()); + // 先删除已有目标文件(避免覆盖失败) + if (newFile.exists()) { + newFile.delete(); } + Files.copy(sourcePath, destPath); + LogUtils.d(TAG, "文件复制成功(Files):" + oldFile.getAbsolutePath() + " → " + newFile.getAbsolutePath()); + return true; + } catch (Exception e) { + LogUtils.e(TAG, "文件复制失败:" + e.getMessage(), e); + return false; } - return false; } + // ====================================== 图片文件相关 ====================================== + + /** + * 从文件路径获取BitmapDrawable(适用于Android图片显示) + * @param path 图片文件绝对路径(非空) + * @return BitmapDrawable 图片对象(文件不存在/读取失败返回null) + * @throws IOException 异常:文件读取IO错误 + */ + public static BitmapDrawable getImageDrawable(String path) throws IOException { + // 1. 校验文件合法性 + File file = new File(path); + if (!file.exists() || !file.isFile()) { + LogUtils.e(TAG, "图片文件不存在:" + path); + return null; + } + + // 2. 读取文件并转为BitmapDrawable(缓冲流读取,减少内存占用) + try (InputStream is = new FileInputStream(file); + ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + + byte[] buffer = new byte[BUFFER_SIZE]; + int readLen; + while ((readLen = is.read(buffer)) != -1) { + bos.write(buffer, 0, readLen); + } + + // 3. 生成Bitmap并包装为BitmapDrawable + byte[] imageBytes = bos.toByteArray(); + Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); + return new BitmapDrawable(bitmap); + } + } + + // ====================================== 文件名处理相关 ====================================== + + /** + * 截取文件后缀名(兼容多 "." 场景,如"image.2025.png" → ".png") + * @param file 目标文件(可为null) + * @return 文件后缀名:带点(如".jpg"),无后缀/文件无效返回空字符串 + */ + public static String getFileSuffixWithMultiDot(File file) { + // 1. 校验文件合法性 + if (file == null || !file.isFile()) { + return ""; + } + + // 2. 提取文件名并查找最后一个 "." + String fileName = file.getName(); + int lastDotIndex = fileName.lastIndexOf("."); + + // 3. 校验后缀合法性(排除无后缀、以点结尾、后缀过长的异常文件) + if (lastDotIndex == -1 // 无 "." + || lastDotIndex == fileName.length() - 1 // 以 "." 结尾(如".gitignore") + || (fileName.length() - lastDotIndex) > 5) { // 后缀长度超过5(异常文件名) + return ""; + } + + // 4. 返回小写后缀(统一格式,避免大小写不一致问题) + return fileName.substring(lastDotIndex).toLowerCase(); + } + + /** + * 生成唯一文件名(优化版:唯一、合法、简洁) + * 生成规则:UUID(去掉"-") + "_" + 时间戳 + 原文件后缀 + * @param refFile 参考文件(用于提取后缀名,可为null) + * @return 唯一文件名(如"a1b2c3d4e5f6_1730000000000.jpg",无后缀则不带点) + */ + public static String createUniqueFileName(File refFile) { + // 1. 获取参考文件的后缀名(自动容错null/无效文件) + String suffix = getFileSuffixWithMultiDot(refFile); + + // 2. 生成唯一标识(UUID确保全局唯一,时间戳进一步降低重复概率) + String uniqueId = UUID.randomUUID().toString().replace("-", ""); // 去掉"-"简化文件名 + long timeStamp = System.currentTimeMillis(); + + // 3. 拼接文件名(分场景处理,避免多余点) + if (suffix.isEmpty()) { + // 无后缀:唯一ID + 时间戳 + return String.format("%s_%d", uniqueId, timeStamp); + } else { + // 有后缀:唯一ID + 时间戳 + 后缀(无多余点) + return String.format("%s_%d%s", uniqueId, timeStamp, suffix); + } + } + + /** + * 复制输入流到文件(兼容Uri解析失败场景) + */ + public static void copyStreamToFile(InputStream inputStream, File file) throws IOException { + if (inputStream == null || file == null) { + throw new IllegalArgumentException("InputStream或File不能为空"); + } + File parentDir = file.getParentFile(); + if (!parentDir.exists() && !parentDir.mkdirs()) { + throw new IOException("无法创建父目录:" + parentDir.getAbsolutePath()); + } + try { + OutputStream outputStream = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + outputStream.flush(); + } finally { + try { + inputStream.close(); + } catch (IOException e) { + LogUtils.e("FileUtils", "关闭输入流失败:" + e.getMessage()); + } + } + } + + 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; + } } + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java new file mode 100644 index 0000000..d1227b9 --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/ImageCropUtils.java @@ -0,0 +1,169 @@ +package cc.winboll.studio.powerbell.utils; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import androidx.core.content.FileProvider; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.powerbell.model.BackgroundBean; +import com.yalantis.ucrop.UCrop; +import com.yalantis.ucrop.UCropActivity; +import java.io.File; +import cc.winboll.studio.powerbell.R; + +/** + * 图片裁剪工具类(集成uCrop,脱离系统依赖) + */ +public class ImageCropUtils { + public static final String TAG = "ImageCropUtils"; + // FileProvider 授权(与项目一致) + private static final String FILE_PROVIDER_SUFFIX = ".fileprovider"; + + /** + * 启动uCrop裁剪(核心方法,替代系统裁剪) + * @param activity 上下文 + * @param inputFile 输入图片文件 + * @param outputFile 输出图片文件 + * @param isFreeCrop 是否自由裁剪(true=自由,false=固定比例) + * @param requestCode 裁剪请求码 + */ + public static void startImageCrop(Activity activity, + File inputFile, + File outputFile, + int aspectX, + int aspectY, + boolean isFreeCrop, + int requestCode) { + // 校验输入参数 + if (activity == null || activity.isFinishing()) { + LogUtils.e(TAG, "【裁剪异常】上下文Activity无效"); + return; + } + if (inputFile == null || !inputFile.exists() || inputFile.length() <= 100) { + LogUtils.e(TAG, "【裁剪异常】输入文件无效"); + showToast(activity, "无有效图片可裁剪"); + return; + } + if (outputFile == null) { + LogUtils.e(TAG, "【裁剪异常】输出文件路径为空"); + showToast(activity, "裁剪输出路径无效"); + return; + } + + // 生成输入/输出Uri(适配FileProvider) + Uri inputUri = getFileProviderUri(activity, inputFile); + Uri outputUri = Uri.fromFile(outputFile); // uCrop 支持直接用文件Uri(兼容低版本) + + // 配置uCrop参数 + UCrop uCrop = UCrop.of(inputUri, outputUri); + UCrop.Options options = new UCrop.Options(); + + // 裁剪模式配置(自由裁剪/固定比例) + 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裁剪Activity(替代系统裁剪) + uCrop.start(activity, requestCode); + + LogUtils.d(TAG, "【uCrop启动】成功,输入Uri:" + inputUri + ",输出Uri:" + outputUri + ",请求码:" + requestCode); + } + + /** + * 重载方法:适配BackgroundBean + */ + public static void startImageCrop(Activity activity, + BackgroundBean cropBean, + int aspectX, + int aspectY, + boolean isFreeCrop, + int requestCode) { + File inputFile = new File(cropBean.getBackgroundFilePath()); + File outputFile = new File(cropBean.getBackgroundScaledCompressFilePath()); + startImageCrop(activity, inputFile, outputFile, aspectX, aspectY, isFreeCrop, requestCode); + } + + /** + * 生成FileProvider Uri + */ + private static Uri getFileProviderUri(Activity activity, File file) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + String authority = activity.getPackageName() + FILE_PROVIDER_SUFFIX; + Uri uri = FileProvider.getUriForFile(activity, authority, file); + LogUtils.d(TAG, "【Uri生成】FileProvider Uri:" + uri); + return uri; + } else { + Uri uri = Uri.fromFile(file); + LogUtils.d(TAG, "【Uri生成】普通Uri:" + uri); + return uri; + } + } catch (Exception e) { + LogUtils.e(TAG, "【Uri生成】失败:" + e.getMessage()); + return null; + } + } + + /** + * 处理uCrop裁剪回调(在Activity的onActivityResult中调用) + * @param requestCode 请求码 + * @param resultCode 结果码 + * @param data 回调数据 + * @return 裁剪成功返回输出文件路径,失败返回null + */ + public static String handleCropResult(int requestCode, int resultCode, Intent data, int cropRequestCode) { + // 校验是否是uCrop的回调 + if (requestCode == cropRequestCode) { + if (resultCode == Activity.RESULT_OK && data != null) { + // 裁剪成功,获取输出Uri + Uri outputUri = UCrop.getOutput(data); + if (outputUri != null) { + String outputPath = outputUri.getPath(); + LogUtils.d(TAG, "【uCrop回调】裁剪成功,输出路径:" + outputPath); + return outputPath; + } + } else if (resultCode == UCrop.RESULT_ERROR) { + // 裁剪失败,获取异常信息 + Throwable error = UCrop.getError(data); + LogUtils.e(TAG, "【uCrop回调】裁剪失败:" + (error != null ? error.getMessage() : "未知错误")); + } else { + LogUtils.d(TAG, "【uCrop回调】裁剪被取消"); + } + } + return null; + } + + /** + * 显示Toast + */ + private static void showToast(Activity activity, String msg) { + if (activity != null && !activity.isFinishing()) { + android.widget.Toast.makeText(activity, msg, android.widget.Toast.LENGTH_SHORT).show(); + } + } + + /** + * 暴露getFileProviderUri方法(供外部调用) + */ + public static Uri getFileProviderUriPublic(Activity activity, File file) { + return getFileProviderUri(activity, file); + } +} + diff --git a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java new file mode 100644 index 0000000..1c510fb --- /dev/null +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PermissionUtils.java @@ -0,0 +1,214 @@ +package cc.winboll.studio.powerbell.utils; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.text.TextUtils; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.powerbell.R; +import java.util.ArrayList; +import java.util.List; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2025/12/01 16:05 + * @Describe 权限申请工具类(单例) + * 核心特性: + * 1. 适配全Android版本(6.0+ 动态权限 / 13+ 兼容) + * 2. 支持多包名场景(无硬编码包名) + * 3. 统一权限校验、申请、回调处理 + * 4. 自带用户引导(拒绝权限+不再询问场景) + */ +public class PermissionUtils { + public static final String TAG = "PermissionUtils"; + // 存储权限请求码(与Activity保持一致,避免冲突) + public static final int STORAGE_PERMISSION_REQUEST2 = 100; + public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 101; + + // 单例实例(双重校验锁,线程安全) + private static volatile PermissionUtils sInstance; + + // 私有构造(禁止外部实例化) + private PermissionUtils() {} + + /** + * 获取单例实例(适配多包名,无硬编码) + */ + public static PermissionUtils getInstance() { + if (sInstance == null) { + synchronized (PermissionUtils.class) { + if (sInstance == null) { + sInstance = new PermissionUtils(); + } + } + } + return sInstance; + } + + // ======================================== 存储权限核心方法 ======================================== + /** + * 检查并申请存储权限(统一入口,适配全Android版本) + * @param activity 上下文(用于权限申请和弹窗) + * @return true:权限已全部获取;false:需要申请权限 + */ + public boolean checkAndRequestStoragePermission(Activity activity) { + if (activity == null || activity.isFinishing()) { + LogUtils.e(TAG, "【权限检查】Activity为空或已销毁,权限检查失败"); + return false; + } + LogUtils.d(TAG, "【权限检查】开始检查存储权限,Android版本:" + Build.VERSION.SDK_INT); + + // 统一使用 WRITE_EXTERNAL_STORAGE + READ_EXTERNAL_STORAGE(适配所有版本,避免READ_MEDIA_IMAGES找不到符号) + String[] requiredPermissions = { + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.Manifest.permission.READ_EXTERNAL_STORAGE + }; + + // 筛选未授予的权限 + List needPermissions = new ArrayList<>(); + for (String permission : requiredPermissions) { + if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + needPermissions.add(permission); + } + } + + // 无权限需要申请:触发动态申请 + if (!needPermissions.isEmpty()) { + String[] permissionsArr = needPermissions.toArray(new String[0]); + ActivityCompat.requestPermissions(activity, permissionsArr, STORAGE_PERMISSION_REQUEST2); + LogUtils.d(TAG, "【权限申请】已触发存储权限申请:" + TextUtils.join(",", permissionsArr)); + return false; + } + + // 所有权限已授予 + LogUtils.d(TAG, "【权限检查】存储权限已全部获取"); + return true; + } + + /** + * 处理存储权限申请回调(统一逻辑,无需在Activity中重复编写) + * @param activity 上下文 + * @param requestCode 请求码(匹配STORAGE_PERMISSION_REQUEST) + * @param permissions 申请的权限数组 + * @param grantResults 权限授予结果数组 + * @return true:回调已处理;false:非当前工具类的权限回调 + */ + public boolean handleStoragePermissionResult(Activity activity, int requestCode, String[] permissions, int[] grantResults) { + // 过滤非存储权限回调 + if (requestCode != STORAGE_PERMISSION_REQUEST2) { + return false; + } + LogUtils.d(TAG, "【权限回调】处理存储权限回调,requestCode:" + requestCode); + + if (activity == null || activity.isFinishing()) { + LogUtils.e(TAG, "【权限回调】Activity为空或已销毁,回调处理终止"); + return true; + } + + // 校验所有权限是否授予 + boolean allGranted = true; + for (int grantResult : grantResults) { + if (grantResult != PackageManager.PERMISSION_GRANTED) { + allGranted = false; + break; + } + } + + if (allGranted) { + // 全部授予:提示用户重新操作 + ToastUtils.show(activity.getString(R.string.permission_grant_success)); + LogUtils.d(TAG, "【权限回调】所有存储权限已授予"); + } else { + // 部分/全部拒绝:判断是否勾选“不再询问” + boolean shouldShowRationale = false; + for (String permission : permissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { + shouldShowRationale = true; + break; + } + } + + if (shouldShowRationale) { + // 未勾选“不再询问”:弹窗引导重新申请 + showPermissionRationaleDialog(activity); + } else { + // 已勾选“不再询问”:引导用户去设置页开启 + showPermissionSettingDialog(activity); + } + LogUtils.d(TAG, "【权限回调】部分/全部存储权限被拒绝,是否需要引导:" + shouldShowRationale); + } + return true; + } + + // ======================================== 辅助方法(私有,封装细节) ======================================== + /** + * 弹窗:未勾选“不再询问”时,提示用户授予权限 + */ + private void showPermissionRationaleDialog(final Activity activity) { + new AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.permission_title)) + .setMessage(activity.getString(R.string.permission_storage_rationale)) + .setPositiveButton(activity.getString(R.string.confirm), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 重新申请权限 + checkAndRequestStoragePermission(activity); + } + }) + .setNegativeButton(activity.getString(R.string.cancel), null) + .show(); + } + + /** + * 弹窗:已勾选“不再询问”时,引导用户去应用设置页开启权限 + */ + private void showPermissionSettingDialog(final Activity activity) { + new AlertDialog.Builder(activity) + .setTitle(activity.getString(R.string.permission_denied_title)) + .setMessage(activity.getString(R.string.permission_storage_setting_guide)) + .setPositiveButton(activity.getString(R.string.go_to_setting), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 跳转应用设置页(适配多包名,动态获取当前包名) + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", activity.getPackageName(), null); + intent.setData(uri); + activity.startActivity(intent); + } + }) + .setNegativeButton(activity.getString(R.string.cancel), null) + .show(); + } + + // ======================================== 扩展:其他权限方法(可选) ======================================== + /** + * 检查单个权限是否已授予(通用方法,可复用) + */ + public boolean isPermissionGranted(Activity activity, String permission) { + if (activity == null || TextUtils.isEmpty(permission)) { + return false; + } + return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED; + } + + /** + * 申请单个权限(通用方法,可复用) + */ + public void requestSinglePermission(Activity activity, String permission, int requestCode) { + if (activity == null || TextUtils.isEmpty(permission)) { + return; + } + if (!isPermissionGranted(activity, permission)) { + ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode); + } + } +} + 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 deleted file mode 100644 index 17e0c3d..0000000 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/utils/PictureUtils.java +++ /dev/null @@ -1,207 +0,0 @@ -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 index 5a63951..e7717bf 100644 --- a/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java +++ b/powerbell/src/main/java/cc/winboll/studio/powerbell/views/BackgroundView.java @@ -4,327 +4,176 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; +import android.graphics.drawable.ColorDrawable; +import android.text.TextUtils; import android.util.AttributeSet; import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; import android.widget.RelativeLayout; import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.powerbell.R; +import cc.winboll.studio.powerbell.model.BackgroundBean; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; /** - * @Author ZhanGSKen&豆包大模型 - * @Date 2025/11/19 18:01 - * @Describe 背景图片视图控件(支持预览临时图片 + 外部刷新) + * 基于Java7的BackgroundView(LinearLayout+ImageView,保持原图比例居中平铺) + * 核心:ImageView保持原图比例,在LinearLayout中居中平铺,无拉伸、无裁剪 */ 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; + private Context mContext; + private LinearLayout mLlContainer; // 主容器LinearLayout + private ImageView mIvBackground; // 图片显示控件 + private float mImageAspectRatio = 1.0f; // 原图宽高比(宽/高) + // ====================================== 构造器(Java7兼容) ====================================== public BackgroundView(Context context) { super(context); + LogUtils.d(TAG, "=== BackgroundView 构造器1 启动 ==="); this.mContext = context; initView(); } public BackgroundView(Context context, AttributeSet attrs) { super(context, attrs); + LogUtils.d(TAG, "=== BackgroundView 构造器2 启动 ==="); this.mContext = context; initView(); } public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + LogUtils.d(TAG, "=== BackgroundView 构造器3 启动 ==="); this.mContext = context; initView(); } - public BackgroundView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - this.mContext = context; - initView(); + // ====================================== 初始化 ====================================== + private void initView() { + LogUtils.d(TAG, "=== initView 启动 ==="); + // 1. 配置当前控件:全屏+透明 + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + setBackgroundColor(0x00000000); + setBackground(new ColorDrawable(0x00000000)); + + // 2. 初始化主容器LinearLayout + initLinearLayout(); + + // 3. 初始化ImageView + initImageView(); + + // 4. 初始设置透明背景 + setDefaultTransparentBackground(); + LogUtils.d(TAG, "=== initView 完成 ==="); } - void initView() { - initBackgroundImageView(); - initBackgroundImagePath(); - loadAndSetImageViewBackground(); + private void initLinearLayout() { + LogUtils.d(TAG, "=== initLinearLayout 启动 ==="); + mLlContainer = new LinearLayout(mContext); + // 配置LinearLayout:全屏+垂直方向+居中 + LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ); + mLlContainer.setLayoutParams(llParams); + mLlContainer.setOrientation(LinearLayout.VERTICAL); + mLlContainer.setGravity(android.view.Gravity.CENTER); // 子View居中 + mLlContainer.setBackgroundColor(0x00000000); + this.addView(mLlContainer); + LogUtils.d(TAG, "=== initLinearLayout 完成 ==="); } - 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 initImageView() { + LogUtils.d(TAG, "=== initImageView 启动 ==="); + mIvBackground = new ImageView(mContext); + // 配置ImageView:wrap_content+居中+透明背景 + LinearLayout.LayoutParams ivParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + mIvBackground.setLayoutParams(ivParams); + mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 保持比例+居中平铺 + mIvBackground.setBackgroundColor(0x00000000); + mLlContainer.addView(mIvBackground); + LogUtils.d(TAG, "=== initImageView 完成 ==="); } - private void initBackgroundImagePath() { - File externalFilesDir = mContext.getExternalFilesDir(null); - if (externalFilesDir == null) { - LogUtils.e(TAG, "外置存储不可用,无法初始化背景图片路径"); - return; - } + public void loadBackgroundBean(BackgroundBean bean) { + if(!bean.isUseBackgroundFile()) { + setDefaultTransparentBackground(); + return; + } + if(bean.isUseBackgroundScaledCompressFile()) { + loadImage(bean.getBackgroundScaledCompressFilePath()); + } else { - File backgroundDir = new File(externalFilesDir, BACKGROUND_IMAGE_FOLDER); - if (!backgroundDir.exists()) { - backgroundDir.mkdirs(); - } - backgroundSourceFilePath = new File(backgroundDir, BACKGROUND_IMAGE_FILENAME).getAbsolutePath(); - } + loadImage(bean.getBackgroundFilePath()); + } + } + // ====================================== 对外方法 ====================================== /** - * 拷贝图片文件到背景资源目录(正式背景) + * 加载图片(保持原图比例,在LinearLayout中居中平铺) + * @param imagePath 图片绝对路径 */ - public void saveToBackgroundSources(String srcBackgroundPath) { - initBackgroundImagePath(); - if (backgroundSourceFilePath == null) { - LogUtils.e(TAG, "目标路径初始化失败,无法保存背景图片"); + public void loadImage(String imagePath) { + LogUtils.d(TAG, "=== loadImage 启动,路径:" + imagePath + " ==="); + if (TextUtils.isEmpty(imagePath)) { + setDefaultTransparentBackground(); return; } - File srcFile = new File(srcBackgroundPath); - if (!srcFile.exists() || !srcFile.isFile()) { - LogUtils.e(TAG, String.format("源文件不存在或不是文件:%s", srcBackgroundPath)); + File imageFile = new File(imagePath); + if (!imageFile.exists() || !imageFile.isFile()) { + LogUtils.e(TAG, "图片文件无效"); + setDefaultTransparentBackground(); 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, "预览图片路径为空"); + // 计算原图比例 + if (!calculateImageAspectRatio(imageFile)) { + setDefaultTransparentBackground(); 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); + // 压缩加载Bitmap + Bitmap bitmap = decodeBitmapWithCompress(imageFile, 1080, 1920); if (bitmap == null) { - LogUtils.e(TAG, "图片加载失败,无法解析为 Bitmap"); - setDefaultImageViewBackground(); + setDefaultTransparentBackground(); 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); + // 设置图片 + mIvBackground.setImageDrawable(new BitmapDrawable(mContext.getResources(), bitmap)); + adjustImageViewSize(); // 调整尺寸 + LogUtils.d(TAG, "=== loadImage 完成 ==="); } - /** - * 计算图片宽高比(宽/高) - */ + // ====================================== 内部工具方法 ====================================== 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); + int width = options.outWidth; + int height = options.outHeight; + if (width <= 0 || height <= 0) { + LogUtils.e(TAG, "图片尺寸无效"); return false; } - imageAspectRatio = (float) imageWidth / imageHeight; + mImageAspectRatio = (float) width / height; + LogUtils.d(TAG, "原图比例:" + mImageAspectRatio); return true; } catch (Exception e) { - LogUtils.e(TAG, "计算图片宽高比失败:" + e.getMessage()); + 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(); @@ -334,41 +183,72 @@ public class BackgroundView extends RelativeLayout { int scaleX = options.outWidth / maxWidth; int scaleY = options.outHeight / maxHeight; int inSampleSize = Math.max(scaleX, scaleY); - if (inSampleSize <= 0) { - inSampleSize = 1; - } + 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()); + LogUtils.e(TAG, "压缩解码失败:" + e.getMessage()); return null; } } /** - * 设置默认背景(图片加载失败时兜底) + * 调整ImageView尺寸(保持原图比例,在LinearLayout中居中平铺) */ - private void setDefaultImageViewBackground() { - ivBackground.setBackgroundResource(R.drawable.default_background); - imageAspectRatio = 1.0f; - adjustImageViewSize(); - LogUtils.d(TAG, "已设置 ImageView 默认背景"); + private void adjustImageViewSize() { + LogUtils.d(TAG, "=== adjustImageViewSize 启动 ==="); + if (mLlContainer == null || mIvBackground == null) { + LogUtils.e(TAG, "控件为空"); + return; + } + + // 获取LinearLayout尺寸 + int llWidth = mLlContainer.getWidth(); + int llHeight = mLlContainer.getHeight(); + if (llWidth == 0 || llHeight == 0) { + postDelayed(new Runnable() { + @Override + public void run() { + adjustImageViewSize(); + } + }, 10); + return; + } + + // 计算ImageView尺寸(保持比例,不超出LinearLayout) + int ivWidth, ivHeight; + if (mImageAspectRatio >= 1.0f) { + ivWidth = Math.min((int) (llHeight * mImageAspectRatio), llWidth); + ivHeight = (int) (ivWidth / mImageAspectRatio); + } else { + ivHeight = Math.min((int) (llWidth / mImageAspectRatio), llHeight); + ivWidth = (int) (ivHeight * mImageAspectRatio); + } + + // 应用尺寸 + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mIvBackground.getLayoutParams(); + params.width = ivWidth; + params.height = ivHeight; + mIvBackground.setLayoutParams(params); + mIvBackground.setScaleType(ScaleType.FIT_CENTER); // 确保居中平铺 + + LogUtils.d(TAG, "ImageView尺寸:" + ivWidth + "x" + ivHeight); + LogUtils.d(TAG, "=== adjustImageViewSize 完成 ==="); } + private void setDefaultTransparentBackground() { + mIvBackground.setImageBitmap(null); + mIvBackground.setBackgroundColor(0x00000000); + mImageAspectRatio = 1.0f; + } + + // ====================================== 重写方法 ====================================== @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - adjustImageViewSize(); - } - - /** - * 对外提供:判断当前是否处于预览模式 - */ - public boolean isPreviewMode() { - return isPreviewMode; + adjustImageViewSize(); // 尺寸变化时重新调整 } } - diff --git a/powerbell/src/main/res/layout/activity_background_settings.xml b/powerbell/src/main/res/layout/activity_background_settings.xml new file mode 100644 index 0000000..c97f8e3 --- /dev/null +++ b/powerbell/src/main/res/layout/activity_background_settings.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/powerbell/src/main/res/layout/activity_backgroundpicture.xml b/powerbell/src/main/res/layout/activity_backgroundpicture.xml deleted file mode 100644 index 40ae2be..0000000 --- a/powerbell/src/main/res/layout/activity_backgroundpicture.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/powerbell/src/main/res/layout/activity_main.xml b/powerbell/src/main/res/layout/activity_main.xml index 92e11e7..d2f7b96 100644 --- a/powerbell/src/main/res/layout/activity_main.xml +++ b/powerbell/src/main/res/layout/activity_main.xml @@ -21,20 +21,20 @@ + android:id="@+id/activitymainRelativeLayout1"/> - + - + diff --git a/powerbell/src/main/res/layout/activity_mainunittest.xml b/powerbell/src/main/res/layout/activity_mainunittest.xml index b8e7d01..583ef92 100644 --- a/powerbell/src/main/res/layout/activity_mainunittest.xml +++ b/powerbell/src/main/res/layout/activity_mainunittest.xml @@ -5,11 +5,47 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> - - + android:background="#FF0C6BBF"> + + + + + + + +