Compare commits
	
		
			5 Commits
		
	
	
		
			positions-
			...
			apputils-v
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a808310a7c | |||
|   | 839a9e2054 | ||
|   | 471ca23585 | ||
|   | 56be1767bb | ||
|   | b66d53da1b | 
| @@ -29,7 +29,7 @@ android { | |||||||
|         // versionName 更新后需要手动设置  |         // versionName 更新后需要手动设置  | ||||||
|         // 项目模块目录的 build.gradle 文件的 stageCount=0 |         // 项目模块目录的 build.gradle 文件的 stageCount=0 | ||||||
|         // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" |         // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" | ||||||
|         versionName "15.8"  |         versionName "15.9"  | ||||||
|         if(true) { |         if(true) { | ||||||
|             versionName = genVersionName("${versionName}") |             versionName = genVersionName("${versionName}") | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| #Created by .winboll/winboll_app_build.gradle | #Created by .winboll/winboll_app_build.gradle | ||||||
| #Mon Sep 01 07:56:33 HKT 2025 | #Mon Sep 22 08:17:49 HKT 2025 | ||||||
| stageCount=7 | stageCount=1 | ||||||
| libraryProject=libapputils | libraryProject=libapputils | ||||||
| baseVersion=15.8 | baseVersion=15.9 | ||||||
| publishVersion=15.8.6 | publishVersion=15.9.0 | ||||||
| buildCount=0 | buildCount=0 | ||||||
| baseBetaVersion=15.8.7 | baseBetaVersion=15.9.1 | ||||||
|   | |||||||
| @@ -2,20 +2,26 @@ | |||||||
| <manifest | <manifest | ||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     package="cc.winboll.studio.apputils"> |     package="cc.winboll.studio.apputils"> | ||||||
|  | 	 | ||||||
|  | 	<!-- 读取外部存储权限(Android 10 以下) --> | ||||||
|  | 	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||||
|  | 		 | ||||||
|     <application |     <application | ||||||
|         android:name=".App" |         android:name=".App" | ||||||
|         android:allowBackup="true" |         android:allowBackup="true" | ||||||
|         android:icon="@drawable/ic_winboll" |         android:icon="@drawable/ic_winboll" | ||||||
|         android:label="@string/app_name" |         android:label="@string/app_name" | ||||||
|         android:theme="@style/MyUtilsTheme" |         android:theme="@style/MyUtilsTheme" | ||||||
|         android:supportsRtl="true"> |         android:supportsRtl="true" | ||||||
|  |         android:resizeableActivity="true" | ||||||
|  | 		android:requestLegacyExternalStorage="true"> | ||||||
|  |  | ||||||
|         <activity |         <activity | ||||||
|             android:name=".MainActivity" |             android:name=".MainActivity" | ||||||
|             android:label="@string/app_name" |             android:label="@string/app_name" | ||||||
|             android:launchMode="singleTask" |             android:launchMode="singleTask" | ||||||
|             android:exported="true"> |             android:exported="true" | ||||||
|  |             android:resizeableActivity="true"> | ||||||
|  |  | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|  |  | ||||||
| @@ -35,6 +41,8 @@ | |||||||
|  |  | ||||||
|         <activity android:name=".QRCodeDecodeActivity"/> |         <activity android:name=".QRCodeDecodeActivity"/> | ||||||
|  |  | ||||||
|  |         <activity android:name=".QRGeneratorActivity"/> | ||||||
|  |  | ||||||
|     </application> |     </application> | ||||||
|  |  | ||||||
| </manifest> | </manifest> | ||||||
|   | |||||||
| @@ -15,12 +15,11 @@ import android.view.MenuItem; | |||||||
| import android.widget.Toolbar; | import android.widget.Toolbar; | ||||||
| import cc.winboll.studio.apputils.R; | import cc.winboll.studio.apputils.R; | ||||||
| import cc.winboll.studio.libappbase.LogUtils; | import cc.winboll.studio.libappbase.LogUtils; | ||||||
| import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity; |  | ||||||
| import cc.winboll.studio.libapputils.views.SimpleWebView; | import cc.winboll.studio.libapputils.views.SimpleWebView; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
|  |  | ||||||
| public class AssetsHtmlActivity extends WinBoLLActivity implements IWinBoLLActivity { | public class AssetsHtmlActivity extends Activity { | ||||||
|  |  | ||||||
|     public static final String TAG = "AssetsHtmlActivity"; |     public static final String TAG = "AssetsHtmlActivity"; | ||||||
|  |  | ||||||
| @@ -32,16 +31,6 @@ public class AssetsHtmlActivity extends WinBoLLActivity implements IWinBoLLActiv | |||||||
|  |  | ||||||
|     // Assets 文件夹里的 Html 文件的名称 |     // Assets 文件夹里的 Html 文件的名称 | ||||||
|     String mszHtmlFileName; |     String mszHtmlFileName; | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public Activity getActivity() { |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public String getTag() { |  | ||||||
|         return TAG; |  | ||||||
|     } |  | ||||||
|      |      | ||||||
|     @Override |     @Override | ||||||
|     public boolean onCreateOptionsMenu(Menu menu) { |     public boolean onCreateOptionsMenu(Menu menu) { | ||||||
|   | |||||||
| @@ -15,9 +15,10 @@ import android.widget.Toolbar; | |||||||
| import cc.winboll.studio.apputils.R; | import cc.winboll.studio.apputils.R; | ||||||
| import cc.winboll.studio.libappbase.LogUtils; | import cc.winboll.studio.libappbase.LogUtils; | ||||||
| import cc.winboll.studio.libappbase.LogView; | import cc.winboll.studio.libappbase.LogView; | ||||||
| import cc.winboll.studio.libappbase.utils.ToastUtils; | import cc.winboll.studio.libappbase.ToastUtils; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
|  | import cc.winboll.studio.libappbase.LogActivity; | ||||||
|  |  | ||||||
| final public class MainActivity extends Activity { | final public class MainActivity extends Activity { | ||||||
|  |  | ||||||
| @@ -145,13 +146,7 @@ final public class MainActivity extends Activity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void onTestLogActivity(View view) { |     public void onTestLogActivity(View view) { | ||||||
| //        Intent intent = new Intent(this, LogActivity.class); | 		LogActivity.startLogActivity(this); | ||||||
| //        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); |  | ||||||
| //        intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); |  | ||||||
| //        startActivity(intent); |  | ||||||
|  |  | ||||||
|         //WinBoLLActivityManager.getInstance().printAvtivityListInfo(); |  | ||||||
|         //WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, LogActivity.class); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // |     // | ||||||
| @@ -217,10 +212,9 @@ final public class MainActivity extends Activity { | |||||||
|         if (item.getItemId() == R.id.item_exit) { |         if (item.getItemId() == R.id.item_exit) { | ||||||
|             //exit(); |             //exit(); | ||||||
|             return true; |             return true; | ||||||
| //        } else if (item.getItemId() == R.id.item_teststringtoqrcodeview) { |         } else if (item.getItemId() == R.id.item_testqrgeneratoractivity) { | ||||||
| //            Intent intent = new Intent(this, TestStringToQRCodeViewActivity.class); |             Intent intent = new Intent(this, QRGeneratorActivity.class); | ||||||
| //            startActivityForResult(intent, REQUEST_QRCODEDECODE_ACTIVITY); |             startActivity(intent); | ||||||
| //            //WinBoLLActivityManager.getInstance(this).startWinBoLLActivity(this, TestStringToQrCodeViewActivity.class); |  | ||||||
|         } else if (item.getItemId() == R.id.item_testqrcodedecodeactivity) { |         } else if (item.getItemId() == R.id.item_testqrcodedecodeactivity) { | ||||||
|             Intent intent = new Intent(this, QRCodeDecodeActivity.class); |             Intent intent = new Intent(this, QRCodeDecodeActivity.class); | ||||||
|             startActivityForResult(intent, REQUEST_QRCODEDECODE_ACTIVITY); |             startActivityForResult(intent, REQUEST_QRCODEDECODE_ACTIVITY); | ||||||
|   | |||||||
| @@ -1,89 +1,323 @@ | |||||||
| package cc.winboll.studio.apputils; | package cc.winboll.studio.apputils; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @Author ZhanGSKen<zhangsken@qq.com> |  * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> | ||||||
|  * @Date 2025/01/18 10:32:21 |  * @Date 2025/01/18 10:32:21 | ||||||
|  * @Describe 二维码扫码解码窗口 |  * @Describe 二维码解码窗口 | ||||||
|  */ |  */ | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
|  | import android.app.AlertDialog; | ||||||
|  | import android.content.DialogInterface; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.pm.PackageManager; | import android.content.pm.PackageManager; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.BitmapFactory; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.ColorMatrix; | ||||||
|  | import android.graphics.ColorMatrixColorFilter; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.net.Uri; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
|  | import android.provider.MediaStore; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.Button; | ||||||
|  | import android.widget.ScrollView; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| import android.widget.Toolbar; | import android.widget.Toolbar; | ||||||
| import cc.winboll.studio.apputils.R; | import cc.winboll.studio.apputils.R; | ||||||
|  | import com.google.zxing.BarcodeFormat; | ||||||
|  | import com.google.zxing.BinaryBitmap; | ||||||
|  | import com.google.zxing.DecodeHintType; | ||||||
|  | import com.google.zxing.LuminanceSource; | ||||||
|  | import com.google.zxing.MultiFormatReader; | ||||||
|  | import com.google.zxing.NotFoundException; | ||||||
|  | import com.google.zxing.RGBLuminanceSource; | ||||||
|  | import com.google.zxing.Result; | ||||||
| import com.google.zxing.ResultPoint; | import com.google.zxing.ResultPoint; | ||||||
|  | import com.google.zxing.common.HybridBinarizer; | ||||||
| import com.journeyapps.barcodescanner.BarcodeCallback; | import com.journeyapps.barcodescanner.BarcodeCallback; | ||||||
| import com.journeyapps.barcodescanner.BarcodeResult; | import com.journeyapps.barcodescanner.BarcodeResult; | ||||||
| import com.journeyapps.barcodescanner.DecoratedBarcodeView; | import com.journeyapps.barcodescanner.DecoratedBarcodeView; | ||||||
|  | import com.journeyapps.barcodescanner.DefaultDecoderFactory; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Hashtable; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| public class QRCodeDecodeActivity extends Activity { | public class QRCodeDecodeActivity extends Activity { | ||||||
|  |  | ||||||
|     public static final String TAG = "QRCodeDecodeActivity"; |     public static final String TAG = "QRCodeDecodeActivity"; | ||||||
|  |  | ||||||
|     public static final String EXTRA_RESULT = "EXTRA_RESULT"; |     public static final String EXTRA_RESULT = "EXTRA_RESULT"; | ||||||
|     private static final int REQUEST_CAMERA_PERMISSION = 1; |     private static final int REQUEST_CAMERA_PERMISSION = 1; | ||||||
|  |     private static final int REQUEST_PICK_IMAGE = 2; | ||||||
|  |     private static final int REQUEST_READ_STORAGE_PERMISSION = 3; | ||||||
|  |     // 图片压缩阈值:超过1000px时压缩,避免内存溢出和解码效率低 | ||||||
|  |     private static final int MAX_BITMAP_SIZE = 1000; | ||||||
|  |  | ||||||
|     TextView resultTextView; |     private TextView resultTextView; | ||||||
|     DecoratedBarcodeView barcodeView; |     private DecoratedBarcodeView barcodeView; | ||||||
|  |     private Button btnDecodeFromAlbum; | ||||||
| //    @Override |  | ||||||
| //    public Activity getActivity() { |  | ||||||
| //        return this; |  | ||||||
| //    } |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onCreate(Bundle savedInstanceState) { |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         setContentView(R.layout.activity_qrcodedecode); |         setContentView(R.layout.activity_qrcodedecode); | ||||||
|  |         initToolbar(); | ||||||
|         // 初始化工具栏 |         initViews(); | ||||||
| //        Toolbar mToolbar = findViewById(R.id.toolbar); |         checkCameraPermission(); | ||||||
| //        setActionBar(mToolbar); |         setAlbumDecodeClickListener(); | ||||||
|  |  | ||||||
|         //resultTextView = findViewById(R.id.activityqrcodedecodeTextView1); |  | ||||||
|         barcodeView = findViewById(R.id.activityqrcodedecodeDecoratedBarcodeView1); |  | ||||||
|         // 请求相机权限 |  | ||||||
| //        if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) |  | ||||||
| //            != PackageManager.PERMISSION_GRANTED) { |  | ||||||
| //            ActivityCompat.requestPermissions(this, |  | ||||||
| //                                              new String[]{android.Manifest.permission.CAMERA}, |  | ||||||
| //                                              REQUEST_CAMERA_PERMISSION); |  | ||||||
| //        } else { |  | ||||||
| //            startScanning(); |  | ||||||
| //        } |  | ||||||
|         startScanning(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private void startScanning() { |     private void initToolbar() { | ||||||
|         barcodeView.getBarcodeView().setDecoderFactory(null); |         Toolbar mToolbar = (Toolbar) findViewById(R.id.toolbar); | ||||||
|         barcodeView.decodeContinuous(barcodeCallback); |         if (mToolbar != null) { | ||||||
|     } |             setActionBar(mToolbar); | ||||||
|  |             if (getActionBar() != null) { | ||||||
|     @Override |                 getActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|     public void onRequestPermissionsResult(int requestCode, |                 mToolbar.setNavigationOnClickListener(new View.OnClickListener() { | ||||||
|                                            String[] permissions, int[] grantResults) { | 						@Override | ||||||
|         if (requestCode == REQUEST_CAMERA_PERMISSION) { | 						public void onClick(View v) { | ||||||
|             if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { | 							finish(); | ||||||
|                 startScanning(); | 						} | ||||||
|             } else { | 					}); | ||||||
|                 // 权限被拒绝的处理 |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     BarcodeCallback barcodeCallback = new BarcodeCallback(){ |     private void initViews() { | ||||||
|  |         resultTextView = (TextView) findViewById(R.id.activityqrcodedecodeTextView1); | ||||||
|  |         barcodeView = (DecoratedBarcodeView) findViewById(R.id.activityqrcodedecodeDecoratedBarcodeView1); | ||||||
|  |         btnDecodeFromAlbum = (Button) findViewById(R.id.btn_decode_from_album); | ||||||
|  |         // 初始化扫码解码器(支持所有常见码制,避免仅支持QR_CODE的局限) | ||||||
|  |         List<BarcodeFormat> formats = new ArrayList<BarcodeFormat>(); | ||||||
|  |         formats.add(BarcodeFormat.QR_CODE); | ||||||
|  |         formats.add(BarcodeFormat.CODE_128); | ||||||
|  |         formats.add(BarcodeFormat.EAN_13); | ||||||
|  |         barcodeView.getBarcodeView().setDecoderFactory(new DefaultDecoderFactory(formats)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void setAlbumDecodeClickListener() { | ||||||
|  |         btnDecodeFromAlbum.setOnClickListener(new View.OnClickListener() { | ||||||
|  | 				@Override | ||||||
|  | 				public void onClick(View v) { | ||||||
|  | 					if (checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) | ||||||
|  |                         != PackageManager.PERMISSION_GRANTED) { | ||||||
|  | 						requestPermissions(new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE}, | ||||||
|  | 										   REQUEST_READ_STORAGE_PERMISSION); | ||||||
|  | 					} else { | ||||||
|  | 						openAlbum(); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void openAlbum() { | ||||||
|  |         Intent pickImageIntent = new Intent(Intent.ACTION_PICK); | ||||||
|  |         pickImageIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); | ||||||
|  |         startActivityForResult(pickImageIntent, REQUEST_PICK_IMAGE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, data); | ||||||
|  |         if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK && data != null) { | ||||||
|  |             Uri selectedImageUri = data.getData(); | ||||||
|  |             if (selectedImageUri != null) { | ||||||
|  |                 try { | ||||||
|  |                     // 1. 读取图片并压缩(关键优化:避免大图片解码失败) | ||||||
|  |                     InputStream imageStream = getContentResolver().openInputStream(selectedImageUri); | ||||||
|  |                     BitmapFactory.Options options = new BitmapFactory.Options(); | ||||||
|  |                     options.inJustDecodeBounds = true; // 先获取图片尺寸,不加载像素 | ||||||
|  |                     BitmapFactory.decodeStream(imageStream, null, options); | ||||||
|  |                     imageStream.close(); // 关闭流,重新读取 | ||||||
|  |  | ||||||
|  |                     // 计算压缩比例:超过MAX_BITMAP_SIZE时按比例压缩 | ||||||
|  |                     options.inSampleSize = calculateInSampleSize(options, MAX_BITMAP_SIZE, MAX_BITMAP_SIZE); | ||||||
|  |                     options.inJustDecodeBounds = false; // 开始加载压缩后的像素 | ||||||
|  |                     imageStream = getContentResolver().openInputStream(selectedImageUri); | ||||||
|  |                     Bitmap originalBitmap = BitmapFactory.decodeStream(imageStream, null, options); | ||||||
|  |                     imageStream.close(); | ||||||
|  |  | ||||||
|  |                     if (originalBitmap == null) { | ||||||
|  |                         showToast("图片损坏,无法解析"); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     // 2. 图片预处理:转为灰度图+提高对比度(解决模糊/低对比度图片识别问题) | ||||||
|  |                     Bitmap processedBitmap = processBitmap(originalBitmap); | ||||||
|  |  | ||||||
|  |                     // 3. 解码预处理后的图片 | ||||||
|  |                     String decodeResult = decodeQrFromBitmap(processedBitmap); | ||||||
|  |  | ||||||
|  |                     // 4. 结果处理 | ||||||
|  |                     if (decodeResult != null && !decodeResult.isEmpty()) { | ||||||
|  |                         resultTextView.setText("图片解码结果:" + decodeResult); | ||||||
|  |                         showDecodeResultDialog(decodeResult); | ||||||
|  |                         returnResultToPreviousPage(decodeResult); | ||||||
|  |                     } else { | ||||||
|  |                         // 尝试直接解码原图(防止预处理过度导致识别失败) | ||||||
|  |                         String originalResult = decodeQrFromBitmap(originalBitmap); | ||||||
|  |                         if (originalResult != null && !originalResult.isEmpty()) { | ||||||
|  |                             resultTextView.setText("图片解码结果:" + originalResult); | ||||||
|  |                             showDecodeResultDialog(originalResult); | ||||||
|  |                             returnResultToPreviousPage(originalResult); | ||||||
|  |                         } else { | ||||||
|  |                             new AlertDialog.Builder(this) | ||||||
|  | 								.setTitle("解码失败") | ||||||
|  | 								.setMessage("图片中未识别到二维码/条码,建议选择清晰、完整的图片") | ||||||
|  | 								.setPositiveButton("确定", new DialogInterface.OnClickListener() { | ||||||
|  | 									@Override | ||||||
|  | 									public void onClick(DialogInterface dialog, int which) { | ||||||
|  | 										dialog.dismiss(); | ||||||
|  | 									} | ||||||
|  | 								}) | ||||||
|  | 								.show(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     // 回收Bitmap,避免内存泄漏 | ||||||
|  |                     if (!originalBitmap.isRecycled()) originalBitmap.recycle(); | ||||||
|  |                     if (!processedBitmap.isRecycled() && processedBitmap != originalBitmap) { | ||||||
|  |                         processedBitmap.recycle(); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                 } catch (Exception e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                     showToast("图片处理失败:" + e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 showToast("未选择图片"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 核心优化1:计算图片压缩比例 | ||||||
|  |      */ | ||||||
|  |     private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { | ||||||
|  |         final int height = options.outHeight; | ||||||
|  |         final int width = options.outWidth; | ||||||
|  |         int inSampleSize = 1; | ||||||
|  |  | ||||||
|  |         if (height > reqHeight || width > reqWidth) { | ||||||
|  |             final int halfHeight = height / 2; | ||||||
|  |             final int halfWidth = width / 2; | ||||||
|  |             // 找到最接近reqWidth/reqHeight的压缩比例(2的倍数,保证图片质量) | ||||||
|  |             while ((halfHeight / inSampleSize) >= reqHeight | ||||||
|  | 				   && (halfWidth / inSampleSize) >= reqWidth) { | ||||||
|  |                 inSampleSize *= 2; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return inSampleSize; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 核心优化2:图片预处理(灰度化+提高对比度) | ||||||
|  |      * 解决模糊、低亮度、低对比度图片识别率低的问题 | ||||||
|  |      */ | ||||||
|  |     private Bitmap processBitmap(Bitmap bitmap) { | ||||||
|  |         int width = bitmap.getWidth(); | ||||||
|  |         int height = bitmap.getHeight(); | ||||||
|  |  | ||||||
|  |         // 创建灰度图 | ||||||
|  |         Bitmap grayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); | ||||||
|  |         Canvas canvas = new Canvas(grayBitmap); | ||||||
|  |         Paint paint = new Paint(); | ||||||
|  |  | ||||||
|  |         // 1. 灰度化矩阵 | ||||||
|  |         ColorMatrix grayMatrix = new ColorMatrix(); | ||||||
|  |         grayMatrix.setSaturation(0); // 饱和度设为0,转为灰度 | ||||||
|  |  | ||||||
|  |         // 2. 提高对比度矩阵(alpha=1.5,亮度=0,可根据需求调整) | ||||||
|  |         ColorMatrix contrastMatrix = new ColorMatrix(); | ||||||
|  |         contrastMatrix.set(new float[]{ | ||||||
|  | 							   1.5f, 0, 0, 0, 0,  // 红通道对比度 | ||||||
|  | 							   0, 1.5f, 0, 0, 0,  // 绿通道对比度 | ||||||
|  | 							   0, 0, 1.5f, 0, 0,  // 蓝通道对比度 | ||||||
|  | 							   0, 0, 0, 1f, 0     // alpha通道不变 | ||||||
|  | 						   }); | ||||||
|  |  | ||||||
|  |         // 合并灰度+对比度矩阵 | ||||||
|  |         ColorMatrix combinedMatrix = new ColorMatrix(); | ||||||
|  |         combinedMatrix.postConcat(grayMatrix); | ||||||
|  |         combinedMatrix.postConcat(contrastMatrix); | ||||||
|  |  | ||||||
|  |         paint.setColorFilter(new ColorMatrixColorFilter(combinedMatrix)); | ||||||
|  |         canvas.drawBitmap(bitmap, 0, 0, paint); | ||||||
|  |  | ||||||
|  |         return grayBitmap; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 核心优化3:修复解码参数,支持更多场景 | ||||||
|  |      */ | ||||||
|  |     private String decodeQrFromBitmap(Bitmap bitmap) { | ||||||
|  |         if (bitmap == null) return null; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             int width = bitmap.getWidth(); | ||||||
|  |             int height = bitmap.getHeight(); | ||||||
|  |             int[] pixels = new int[width * height]; | ||||||
|  |             bitmap.getPixels(pixels, 0, width, 0, 0, width, height); | ||||||
|  |  | ||||||
|  |             // 修复1:使用RGBLuminanceSource,避免YUV格式导致的颜色偏差 | ||||||
|  |             LuminanceSource source = new RGBLuminanceSource(width, height, pixels); | ||||||
|  |             BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source)); | ||||||
|  |  | ||||||
|  |             // 修复2:完善解码参数,解决模糊、变形二维码识别问题 | ||||||
|  |             Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>(); | ||||||
|  |             // 支持所有常见码制(不仅限于QR_CODE) | ||||||
|  |             List<BarcodeFormat> formats = new ArrayList<BarcodeFormat>(); | ||||||
|  |             formats.add(BarcodeFormat.QR_CODE); | ||||||
|  |             formats.add(BarcodeFormat.CODE_128); | ||||||
|  |             formats.add(BarcodeFormat.EAN_8); | ||||||
|  |             formats.add(BarcodeFormat.EAN_13); | ||||||
|  |             hints.put(DecodeHintType.POSSIBLE_FORMATS, formats); | ||||||
|  |             // 字符编码:支持中文等多语言 | ||||||
|  |             hints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); | ||||||
|  |             // 容错模式:允许二维码有一定损坏(关键!解决轻微变形/污染的二维码) | ||||||
|  |             hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); | ||||||
|  |             // 不使用纯条码模式,兼容带logo的二维码 | ||||||
|  |             hints.put(DecodeHintType.PURE_BARCODE, Boolean.FALSE); | ||||||
|  |  | ||||||
|  |             MultiFormatReader reader = new MultiFormatReader(); | ||||||
|  |             reader.setHints(hints); | ||||||
|  |             Result result = reader.decode(binaryBitmap); | ||||||
|  |             return result.getText(); | ||||||
|  |  | ||||||
|  |         } catch (NotFoundException e) { | ||||||
|  |             // 正常未识别到,不打印异常(避免日志冗余) | ||||||
|  |             return null; | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // ==================== 原有逻辑(不变) ==================== | ||||||
|  |     private void checkCameraPermission() { | ||||||
|  |         if (checkSelfPermission(android.Manifest.permission.CAMERA) | ||||||
|  | 			!= PackageManager.PERMISSION_GRANTED) { | ||||||
|  |             requestPermissions(new String[]{android.Manifest.permission.CAMERA}, | ||||||
|  | 							   REQUEST_CAMERA_PERMISSION); | ||||||
|  |         } else { | ||||||
|  |             startScanning(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void startScanning() { | ||||||
|  |         barcodeView.decodeContinuous(barcodeCallback); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private BarcodeCallback barcodeCallback = new BarcodeCallback() { | ||||||
|         @Override |         @Override | ||||||
|         public void barcodeResult(BarcodeResult result) { |         public void barcodeResult(BarcodeResult result) { | ||||||
|             if (result.getText() != null) { |             if (result != null && result.getText() != null) { | ||||||
|                 //Toast.makeText(MainActivity.this, "Scanned: " + result.getText(), Toast.LENGTH_SHORT).show(); |  | ||||||
|                 //ToastUtils.show("Scanned: " + result.getText()); |  | ||||||
|                 barcodeView.pause(); |                 barcodeView.pause(); | ||||||
|                 Intent intent = new Intent(); |                 String decodeResult = result.getText(); | ||||||
|                 intent.putExtra(EXTRA_RESULT, result.getText()); |                 resultTextView.setText("扫码结果:" + decodeResult); | ||||||
|                 setResult(RESULT_OK, intent); |                 showDecodeResultDialog(decodeResult); | ||||||
|                 finish(); |                 returnResultToPreviousPage(decodeResult); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -92,16 +326,111 @@ public class QRCodeDecodeActivity extends Activity { | |||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     private void showDecodeResultDialog(String result) { | ||||||
|  |         ScrollView scrollView = new ScrollView(this); | ||||||
|  |         scrollView.setPadding(dip2px(16), dip2px(16), dip2px(16), dip2px(16)); | ||||||
|  |  | ||||||
|  |         TextView dialogTv = new TextView(this); | ||||||
|  |         dialogTv.setTextSize(16); | ||||||
|  |         dialogTv.setTextColor(getResources().getColor(android.R.color.black)); | ||||||
|  |         dialogTv.setText(result); | ||||||
|  |         scrollView.addView(dialogTv); | ||||||
|  |  | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(this); | ||||||
|  |         builder.setTitle("解码结果"); | ||||||
|  |         builder.setView(scrollView); | ||||||
|  |         builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { | ||||||
|  | 				@Override | ||||||
|  | 				public void onClick(DialogInterface dialog, int which) { | ||||||
|  | 					dialog.dismiss(); | ||||||
|  | 					barcodeView.resume(); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  |         builder.setCancelable(false); | ||||||
|  |         builder.show(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void returnResultToPreviousPage(String result) { | ||||||
|  |         Intent intent = new Intent(); | ||||||
|  |         intent.putExtra(EXTRA_RESULT, result); | ||||||
|  |         setResult(RESULT_OK, intent); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { | ||||||
|  |         super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||||
|  |         if (requestCode == REQUEST_CAMERA_PERMISSION) { | ||||||
|  |             if (grantResults != null && grantResults.length > 0) { | ||||||
|  |                 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||||||
|  |                     startScanning(); | ||||||
|  |                 } else { | ||||||
|  |                     new AlertDialog.Builder(this) | ||||||
|  | 						.setTitle("权限申请") | ||||||
|  | 						.setMessage("扫码需要相机权限,请在设置中开启") | ||||||
|  | 						.setPositiveButton("确定", new DialogInterface.OnClickListener() { | ||||||
|  | 							@Override | ||||||
|  | 							public void onClick(DialogInterface dialog, int which) { | ||||||
|  | 								dialog.dismiss(); | ||||||
|  | 								finish(); | ||||||
|  | 							} | ||||||
|  | 						}) | ||||||
|  | 						.setCancelable(false) | ||||||
|  | 						.show(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else if (requestCode == REQUEST_READ_STORAGE_PERMISSION) { | ||||||
|  |             if (grantResults != null && grantResults.length > 0) { | ||||||
|  |                 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||||||
|  |                     openAlbum(); | ||||||
|  |                 } else { | ||||||
|  |                     new AlertDialog.Builder(this) | ||||||
|  | 						.setTitle("权限申请") | ||||||
|  | 						.setMessage("从相册解码需要存储权限,请在设置中开启") | ||||||
|  | 						.setPositiveButton("确定", new DialogInterface.OnClickListener() { | ||||||
|  | 							@Override | ||||||
|  | 							public void onClick(DialogInterface dialog, int which) { | ||||||
|  | 								dialog.dismiss(); | ||||||
|  | 							} | ||||||
|  | 						}) | ||||||
|  | 						.setCancelable(false) | ||||||
|  | 						.show(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onResume() { |     protected void onResume() { | ||||||
|         super.onResume(); |         super.onResume(); | ||||||
|         barcodeView.resume(); |         if (barcodeView != null) { | ||||||
|  |             barcodeView.resume(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected void onPause() { |     protected void onPause() { | ||||||
|         super.onPause(); |         super.onPause(); | ||||||
|         barcodeView.pause(); |         if (barcodeView != null) { | ||||||
|  |             barcodeView.pause(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int dip2px(float dpValue) { | ||||||
|  |         final float scale = getResources().getDisplayMetrics().density; | ||||||
|  |         return (int) (dpValue * scale + 0.5f); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(android.view.MenuItem item) { | ||||||
|  |         if (item.getItemId() == android.R.id.home) { | ||||||
|  |             finish(); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         return super.onOptionsItemSelected(item); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void showToast(String message) { | ||||||
|  |         android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | package cc.winboll.studio.apputils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com> | ||||||
|  |  * @Date 2025/09/22 07:09 | ||||||
|  |  * @Describe 二维码生成窗口 | ||||||
|  |  */ | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.Button; | ||||||
|  | import android.widget.EditText; | ||||||
|  | import android.widget.ImageView; | ||||||
|  | import android.widget.Toast; | ||||||
|  | import com.google.zxing.BarcodeFormat; | ||||||
|  | import com.google.zxing.WriterException; | ||||||
|  | import com.journeyapps.barcodescanner.BarcodeEncoder; | ||||||
|  |  | ||||||
|  | public class QRGeneratorActivity extends Activity { | ||||||
|  | 	public static final String TAG = "QrGeneratorActivity"; | ||||||
|  |  | ||||||
|  | 	// 控件引用 | ||||||
|  | 	private EditText etInputText; | ||||||
|  | 	private ImageView ivQrPreview; | ||||||
|  | 	private Button btnGenerateQr; | ||||||
|  |  | ||||||
|  | 	@Override | ||||||
|  | 	protected void onCreate(Bundle savedInstanceState) { | ||||||
|  | 		super.onCreate(savedInstanceState); | ||||||
|  | 		setContentView(R.layout.activity_qrgenerator); | ||||||
|  |  | ||||||
|  | 		// 初始化控件 | ||||||
|  | 		initViews(); | ||||||
|  | 		// 设置按钮点击事件 | ||||||
|  | 		setGenerateClickListener(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 初始化布局控件 | ||||||
|  | 	 */ | ||||||
|  | 	private void initViews() { | ||||||
|  | 		etInputText = findViewById(R.id.et_input_text); | ||||||
|  | 		ivQrPreview = findViewById(R.id.iv_qr_preview); | ||||||
|  | 		btnGenerateQr = findViewById(R.id.btn_generate_qr); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 设置生成按钮点击事件:获取输入文字 → 生成二维码 → 显示到 ImageView | ||||||
|  | 	 */ | ||||||
|  | 	private void setGenerateClickListener() { | ||||||
|  | 		btnGenerateQr.setOnClickListener(new View.OnClickListener() { | ||||||
|  | 				@Override | ||||||
|  | 				public void onClick(View v) { | ||||||
|  | 					// 1. 获取输入框文字(去除前后空格) | ||||||
|  | 					String inputText = etInputText.getText().toString().trim(); | ||||||
|  |  | ||||||
|  | 					// 2. 空输入判断 | ||||||
|  | 					if (inputText.isEmpty()) { | ||||||
|  | 						Toast.makeText(QRGeneratorActivity.this, "请先输入要生成二维码的文字", Toast.LENGTH_SHORT).show(); | ||||||
|  | 						return; | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					// 3. 生成二维码 Bitmap(宽高 500px,可调整) | ||||||
|  | 					Bitmap qrBitmap = generateQrCodeBitmap(inputText, 500, 500); | ||||||
|  |  | ||||||
|  | 					// 4. 显示二维码到 ImageView | ||||||
|  | 					if (qrBitmap != null) { | ||||||
|  | 						ivQrPreview.setImageBitmap(qrBitmap); | ||||||
|  | 					} else { | ||||||
|  | 						Toast.makeText(QRGeneratorActivity.this, "二维码生成失败,请重试", Toast.LENGTH_SHORT).show(); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 核心方法:生成二维码 Bitmap | ||||||
|  | 	 * @param content 二维码内容(输入的文字) | ||||||
|  | 	 * @param width 二维码宽度(px) | ||||||
|  | 	 * @param height 二维码高度(px) | ||||||
|  | 	 * @return 生成的二维码 Bitmap,失败返回 null | ||||||
|  | 	 */ | ||||||
|  | 	private Bitmap generateQrCodeBitmap(String content, int width, int height) { | ||||||
|  | 		try { | ||||||
|  | 			// 初始化二维码编码器(指定格式为 QR_CODE) | ||||||
|  | 			BarcodeEncoder encoder = new BarcodeEncoder(); | ||||||
|  | 			// 生成二维码 Bitmap(参数:内容、格式、宽、高) | ||||||
|  | 			return encoder.encodeBitmap(content, BarcodeFormat.QR_CODE, width, height); | ||||||
|  | 		} catch (WriterException e) { | ||||||
|  | 			// 生成失败(如内容过长、宽高非法),打印异常信息 | ||||||
|  | 			e.printStackTrace(); | ||||||
|  | 			return null; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -6,19 +6,9 @@ package cc.winboll.studio.apputils; | |||||||
|  * @Describe WinBoLLActivity |  * @Describe WinBoLLActivity | ||||||
|  */ |  */ | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import cc.winboll.studio.libappbase.winboll.IWinBoLLActivity; |  | ||||||
|  |  | ||||||
| public class WinBoLLActivity extends Activity implements IWinBoLLActivity { | public class WinBoLLActivity extends Activity { | ||||||
|  |  | ||||||
|     public static final String TAG = "WinBoLLActivity"; |     public static final String TAG = "WinBoLLActivity"; | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public Activity getActivity() { |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public String getTag() { |  | ||||||
|         return TAG; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,20 +1,43 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <LinearLayout | <LinearLayout  | ||||||
|     xmlns:android="http://schemas.android.com/apk/res/android" |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |  | ||||||
|     android:orientation="vertical" |  | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent"> |     android:layout_height="match_parent" | ||||||
|  |     android:orientation="vertical"> | ||||||
|  |  | ||||||
|     <TextView |     <!-- 工具栏(原有) --> | ||||||
|         android:layout_width="wrap_content" |     <Toolbar | ||||||
|         android:layout_height="wrap_content" |         android:id="@+id/toolbar" | ||||||
|         android:text="QRCodeDecodeActivity"/> |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content"/> | ||||||
|  |  | ||||||
|  |     <!-- 扫码控件(原有) --> | ||||||
|     <com.journeyapps.barcodescanner.DecoratedBarcodeView |     <com.journeyapps.barcodescanner.DecoratedBarcodeView | ||||||
|         android:layout_width="wrap_content" |         android:id="@+id/activityqrcodedecodeDecoratedBarcodeView1" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:layout_weight="1"/> | ||||||
|  |  | ||||||
|  |     <!-- 新增:从相册解码按钮 --> | ||||||
|  |     <Button | ||||||
|  |         android:id="@+id/btn_decode_from_album" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:id="@+id/activityqrcodedecodeDecoratedBarcodeView1"/> |         android:text="从相册选图解码" | ||||||
|  |         android:textSize="16sp" | ||||||
|  |         android:textColor="@android:color/white" | ||||||
|  |         android:background="#2196F3" | ||||||
|  |         android:paddingVertical="12dp" | ||||||
|  |         android:layout_margin="16dp"/> | ||||||
|  |  | ||||||
|  |     <!-- 结果显示TextView(原有) --> | ||||||
|  |     <TextView | ||||||
|  |         android:id="@+id/activityqrcodedecodeTextView1" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:padding="16dp" | ||||||
|  |         android:text="等待扫码或选择图片..." | ||||||
|  |         android:textSize="16sp"/> | ||||||
|  |  | ||||||
| </LinearLayout> | </LinearLayout> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								apputils/src/main/res/layout/activity_qrgenerator.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								apputils/src/main/res/layout/activity_qrgenerator.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout | ||||||
|  |     xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:orientation="vertical" | ||||||
|  |     android:padding="20dp" | ||||||
|  |     android:gravity="top|center_horizontal"> | ||||||
|  |  | ||||||
|  |     <!-- 文字输入框 --> | ||||||
|  |     <EditText | ||||||
|  |         android:id="@+id/et_input_text" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:hint="请输入要生成二维码的文字" | ||||||
|  |         android:inputType="textMultiLine" | ||||||
|  |         android:minLines="3" | ||||||
|  |         android:maxLines="5" | ||||||
|  |         android:padding="12dp" | ||||||
|  |         android:background="@android:drawable/editbox_background_normal" | ||||||
|  |         android:layout_marginBottom="16dp"/> | ||||||
|  |  | ||||||
|  |     <!-- 生成二维码按钮 --> | ||||||
|  |     <Button | ||||||
|  |         android:id="@+id/btn_generate_qr" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:text="生成二维码" | ||||||
|  |         android:textSize="16sp" | ||||||
|  |         android:background="#4CAF50" | ||||||
|  |         android:textColor="@android:color/white" | ||||||
|  |         android:paddingVertical="12dp" | ||||||
|  |         android:layout_marginBottom="24dp"/> | ||||||
|  |  | ||||||
|  |     <!-- 二维码预览图片 --> | ||||||
|  |     <ImageView | ||||||
|  |         android:id="@+id/iv_qr_preview" | ||||||
|  |         android:layout_width="280dp" | ||||||
|  |         android:layout_height="280dp" | ||||||
|  |         android:scaleType="fitCenter" | ||||||
|  |         android:background="@android:drawable/dialog_holo_light_frame" | ||||||
|  |         android:contentDescription="二维码预览"/> | ||||||
|  |  | ||||||
|  | </LinearLayout> | ||||||
|  |  | ||||||
| @@ -2,8 +2,8 @@ | |||||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
| 	xmlns:app="http://schemas.android.com/apk/res-auto"> | 	xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||||
|     <item |     <item | ||||||
|         android:id="@+id/item_teststringtoqrcodeview" |         android:id="@+id/item_testqrgeneratoractivity" | ||||||
|         android:title="TestStringToQRCodeViewActivity"/> |         android:title="TestQRGeneratorActivity"/> | ||||||
|     <item |     <item | ||||||
|         android:id="@+id/item_testqrcodedecodeactivity" |         android:id="@+id/item_testqrcodedecodeactivity" | ||||||
|         android:title="TestQRCodeDecodeActivity"/> |         android:title="TestQRCodeDecodeActivity"/> | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								keystore
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								keystore
									
									
									
									
									
										Submodule
									
								
							 Submodule keystore added at e7f70226c1
									
								
							| @@ -21,7 +21,7 @@ android { | |||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|     api fileTree(dir: 'libs', include: ['*.jar']) |     api fileTree(dir: 'libs', include: ['*.jar']) | ||||||
|     api 'cc.winboll.studio:libappbase:15.9.5' |     api 'cc.winboll.studio:libappbase:15.10.7' | ||||||
|      |      | ||||||
|     // 二维码类库 |     // 二维码类库 | ||||||
|     api 'com.google.zxing:core:3.4.1' |     api 'com.google.zxing:core:3.4.1' | ||||||
| @@ -32,6 +32,8 @@ dependencies { | |||||||
|      |      | ||||||
|     // Html 解析 |     // Html 解析 | ||||||
|     api 'org.jsoup:jsoup:1.13.1' |     api 'org.jsoup:jsoup:1.13.1' | ||||||
|  | 	 | ||||||
|  | 	api 'com.google.code.gson:gson:2.10.1' | ||||||
|      |      | ||||||
|     // SSH |     // SSH | ||||||
|     //api 'com.jcraft:jsch:0.1.55' |     //api 'com.jcraft:jsch:0.1.55' | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| #Created by .winboll/winboll_app_build.gradle | #Created by .winboll/winboll_app_build.gradle | ||||||
| #Mon Sep 01 07:56:11 HKT 2025 | #Mon Sep 22 08:17:49 HKT 2025 | ||||||
| stageCount=7 | stageCount=1 | ||||||
| libraryProject=libapputils | libraryProject=libapputils | ||||||
| baseVersion=15.8 | baseVersion=15.9 | ||||||
| publishVersion=15.8.6 | publishVersion=15.9.0 | ||||||
| buildCount=0 | buildCount=0 | ||||||
| baseBetaVersion=15.8.7 | baseBetaVersion=15.9.1 | ||||||
|   | |||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package cc.winboll.studio.libapputils.models; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen<zhangsken@qq.com> | ||||||
|  |  * @Date 2025/06/05 11:26 | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | public class ResponseData { | ||||||
|  |      | ||||||
|  |     public static final String STATUS_SUCCESS = "success"; | ||||||
|  |     public static final String STATUS_ERROR = "error"; | ||||||
|  |      | ||||||
|  |     private String status; | ||||||
|  |     private String message; | ||||||
|  |     private UserInfoModel data; | ||||||
|  |      | ||||||
|  |     public ResponseData() { | ||||||
|  |         this.status = ""; | ||||||
|  |         this.message = ""; | ||||||
|  |         this.data = new UserInfoModel(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public ResponseData(String status, String message, UserInfoModel data) { | ||||||
|  |         this.status = status; | ||||||
|  |         this.message = message; | ||||||
|  |         this.data = data; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setStatus(String status) { | ||||||
|  |         this.status = status; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getStatus() { | ||||||
|  |         return status; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setMessage(String message) { | ||||||
|  |         this.message = message; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getMessage() { | ||||||
|  |         return message; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setData(UserInfoModel data) { | ||||||
|  |         this.data = data; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public UserInfoModel getData() { | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,92 @@ | |||||||
|  | package cc.winboll.studio.libapputils.models; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen<zhangsken@qq.com> | ||||||
|  |  * @Date 2025/06/04 19:14 | ||||||
|  |  */ | ||||||
|  | import android.util.JsonReader; | ||||||
|  | import android.util.JsonWriter; | ||||||
|  | import cc.winboll.studio.libappbase.BaseBean; | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | public class UserInfoModel extends BaseBean { | ||||||
|  |  | ||||||
|  |     public static final String TAG = "UserInfoModel"; | ||||||
|  |  | ||||||
|  |     String username; | ||||||
|  |     String password; | ||||||
|  |     String token; | ||||||
|  |  | ||||||
|  |     public UserInfoModel() { | ||||||
|  |         this.username = ""; | ||||||
|  |         this.password = ""; | ||||||
|  |         this.token = ""; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setUsername(String username) { | ||||||
|  |         this.username = username; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getUsername() { | ||||||
|  |         return username; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setPassword(String password) { | ||||||
|  |         this.password = password; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getPassword() { | ||||||
|  |         return password; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setToken(String token) { | ||||||
|  |         this.token = token; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public String getToken() { | ||||||
|  |         return token; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getName() { | ||||||
|  |         return UserInfoModel.class.getName(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException { | ||||||
|  |         super.writeThisToJsonWriter(jsonWriter); | ||||||
|  |         jsonWriter.name("username").value(getUsername()); | ||||||
|  |         jsonWriter.name("password").value(getPassword()); | ||||||
|  |         jsonWriter.name("token").value(getToken()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException { | ||||||
|  |         if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else { | ||||||
|  |             if (name.equals("username")) { | ||||||
|  |                 setUsername(jsonReader.nextString()); | ||||||
|  |             } else if (name.equals("password")) { | ||||||
|  |                 setPassword(jsonReader.nextString()); | ||||||
|  |             } else if (name.equals("token")) { | ||||||
|  |                 setToken(jsonReader.nextString()); | ||||||
|  |             } else { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException { | ||||||
|  |         jsonReader.beginObject(); | ||||||
|  |         while (jsonReader.hasNext()) { | ||||||
|  |             String name = jsonReader.nextName(); | ||||||
|  |             if (!initObjectsFromJsonReader(jsonReader, name)) { | ||||||
|  |                 jsonReader.skipValue(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // 结束 JSON 对象 | ||||||
|  |         jsonReader.endObject(); | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | package cc.winboll.studio.libapputils.utils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen<zhangsken@qq.com> | ||||||
|  |  * @Date 2025/02/15 20:05:03 | ||||||
|  |  * @Describe AppUtils | ||||||
|  |  */ | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.pm.ApplicationInfo; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | import android.content.pm.PackageManager.NameNotFoundException; | ||||||
|  | import cc.winboll.studio.libappbase.LogUtils; | ||||||
|  |  | ||||||
|  | public class AppUtils { | ||||||
|  |      | ||||||
|  |     public static final String TAG = "AppUtils"; | ||||||
|  |      | ||||||
|  |     public static String getAppNameByPackageName(Context context, String packageName) { | ||||||
|  |         PackageManager packageManager = context.getPackageManager(); | ||||||
|  |         try { | ||||||
|  |             ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0); | ||||||
|  |             return (String) packageManager.getApplicationLabel(applicationInfo); | ||||||
|  |         } catch (NameNotFoundException e) { | ||||||
|  |             LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); | ||||||
|  |             return ""; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -9,10 +9,16 @@ import android.content.Context; | |||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| import android.content.res.AssetManager; | import android.content.res.AssetManager; | ||||||
| import android.net.Uri; | import android.net.Uri; | ||||||
|  | import android.support.v4.content.FileProvider; | ||||||
| import cc.winboll.studio.libappbase.LogUtils; | import cc.winboll.studio.libappbase.LogUtils; | ||||||
|  | import java.io.BufferedReader; | ||||||
|  | import java.io.BufferedWriter; | ||||||
|  | import java.io.ByteArrayOutputStream; | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileInputStream; | import java.io.FileInputStream; | ||||||
| import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||||
|  | import java.io.FileReader; | ||||||
|  | import java.io.FileWriter; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||||
| @@ -22,7 +28,6 @@ import java.nio.charset.StandardCharsets; | |||||||
| import java.nio.file.Files; | import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | import java.nio.file.Path; | ||||||
| import java.nio.file.Paths; | import java.nio.file.Paths; | ||||||
| import android.support.v4.content.FileProvider; |  | ||||||
|  |  | ||||||
| public class FileUtils { | public class FileUtils { | ||||||
|  |  | ||||||
| @@ -97,36 +102,6 @@ public class FileUtils { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // |  | ||||||
|     // 把字符串写入文件,指定 UTF-8 编码 |  | ||||||
|     // |  | ||||||
|     public static void writeStringToFile(String szFilePath, String szContent) throws IOException { |  | ||||||
|         File file = new File(szFilePath); |  | ||||||
|         if (!file.getParentFile().exists()) { |  | ||||||
|             file.getParentFile().mkdirs(); |  | ||||||
|         } |  | ||||||
|         FileOutputStream outputStream = new FileOutputStream(file); |  | ||||||
|         OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); |  | ||||||
|         writer.write(szContent); |  | ||||||
|         writer.close(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // |  | ||||||
|     // 读取文件到字符串,指定 UTF-8 编码 |  | ||||||
|     // |  | ||||||
|     public static String readStringFromFile(String szFilePath) throws IOException { |  | ||||||
|         File file = new File(szFilePath); |  | ||||||
|         FileInputStream inputStream = new FileInputStream(file); |  | ||||||
|         InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); |  | ||||||
|         StringBuilder content = new StringBuilder(); |  | ||||||
|         int character; |  | ||||||
|         while ((character = reader.read()) != -1) { |  | ||||||
|             content.append((char) character); |  | ||||||
|         } |  | ||||||
|         reader.close(); |  | ||||||
|         return content.toString(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static boolean copyFile(File srcFile, File dstFile) { |     public static boolean copyFile(File srcFile, File dstFile) { | ||||||
|         if (!srcFile.exists()) { |         if (!srcFile.exists()) { | ||||||
|             LogUtils.d(TAG, "The original file does not exist."); |             LogUtils.d(TAG, "The original file does not exist."); | ||||||
| @@ -154,4 +129,113 @@ public class FileUtils { | |||||||
|         } |         } | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  | 	 | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 读取文件为字节数组(Java 7 语法) | ||||||
|  |      */ | ||||||
|  |     public static byte[] readByteArrayFromFile(String filePath) { | ||||||
|  |         FileInputStream fis = null; | ||||||
|  |         ByteArrayOutputStream bos = null; | ||||||
|  |         try { | ||||||
|  |             fis = new FileInputStream(filePath); | ||||||
|  |             bos = new ByteArrayOutputStream(); | ||||||
|  |             byte[] buffer = new byte[4096]; | ||||||
|  |             int bytesRead; | ||||||
|  |             while ((bytesRead = fis.read(buffer)) != -1) { | ||||||
|  |                 bos.write(buffer, 0, bytesRead); | ||||||
|  |             } | ||||||
|  |             return bos.toByteArray(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |             return null; | ||||||
|  |         } finally { | ||||||
|  |             // 手动关闭流(Java 7 不支持 try-with-resources) | ||||||
|  |             if (fis != null) { | ||||||
|  |                 try { | ||||||
|  |                     fis.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             if (bos != null) { | ||||||
|  |                 try { | ||||||
|  |                     bos.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 写入字节数组到文件(Java 7 语法) | ||||||
|  |      */ | ||||||
|  |     public static boolean writeByteArrayToFile(byte[] data, String filePath) { | ||||||
|  |         FileOutputStream fos = null; | ||||||
|  |         try { | ||||||
|  |             fos = new FileOutputStream(filePath); | ||||||
|  |             fos.write(data); | ||||||
|  |             return true; | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |             return false; | ||||||
|  |         } finally { | ||||||
|  |             if (fos != null) { | ||||||
|  |                 try { | ||||||
|  |                     fos.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public static String readStringFromFile(String filePath) { | ||||||
|  |         BufferedReader reader = null; | ||||||
|  |         try { | ||||||
|  |             reader = new BufferedReader(new FileReader(filePath)); | ||||||
|  |             StringBuilder content = new StringBuilder(); | ||||||
|  |             String line; | ||||||
|  |             while ((line = reader.readLine()) != null) { | ||||||
|  |                 content.append(line).append(System.getProperty("line.separator")); | ||||||
|  |             } | ||||||
|  |             // 去除最后一个换行符(可选) | ||||||
|  |             if (content.length() > 0) { | ||||||
|  |                 content.deleteCharAt(content.length() - 1); | ||||||
|  |             } | ||||||
|  |             return content.toString(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |             return null; | ||||||
|  |         } finally { | ||||||
|  |             if (reader != null) { | ||||||
|  |                 try { | ||||||
|  |                     reader.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 	 | ||||||
|  |     public static boolean writeStringToFile(String content, String filePath, boolean append) { | ||||||
|  |         BufferedWriter writer = null; | ||||||
|  |         try { | ||||||
|  |             writer = new BufferedWriter(new FileWriter(filePath, append)); | ||||||
|  |             writer.write(content); | ||||||
|  |             return true; | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |             return false; | ||||||
|  |         } finally { | ||||||
|  |             if (writer != null) { | ||||||
|  |                 try { | ||||||
|  |                     writer.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,222 @@ | |||||||
|  | package cc.winboll.studio.libapputils.utils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen<zhangsken@qq.com> | ||||||
|  |  * @Date 2025/06/04 13:36 | ||||||
|  |  * @Describe RSA加密工具 | ||||||
|  |  */ | ||||||
|  | import android.content.Context; | ||||||
|  | import android.util.Base64; | ||||||
|  | import cc.winboll.studio.libappbase.LogUtils; | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileInputStream; | ||||||
|  | import java.io.FileOutputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.security.KeyFactory; | ||||||
|  | import java.security.KeyPair; | ||||||
|  | import java.security.KeyPairGenerator; | ||||||
|  | import java.security.NoSuchAlgorithmException; | ||||||
|  | import java.security.PrivateKey; | ||||||
|  | import java.security.PublicKey; | ||||||
|  | import java.security.spec.InvalidKeySpecException; | ||||||
|  | import java.security.spec.PKCS8EncodedKeySpec; | ||||||
|  | import java.security.spec.X509EncodedKeySpec; | ||||||
|  | import java.util.Objects; | ||||||
|  | import javax.crypto.Cipher; | ||||||
|  |  | ||||||
|  | public class RSAUtils { | ||||||
|  |     private static final String TAG = "RSAUtils"; | ||||||
|  |     private static final int KEY_SIZE = 2048; | ||||||
|  |     private static final String KEY_ALGORITHM = "RSA"; | ||||||
|  |     private static final String PUBLIC_KEY_FILE = "public.key"; | ||||||
|  |     private static final String PRIVATE_KEY_FILE = "private.key"; | ||||||
|  |     private static final String CIPHER_ALGORITHM = KEY_ALGORITHM + "/ECB/PKCS1Padding"; // 保留原加密方式 | ||||||
|  |  | ||||||
|  |     private final String keyPath; | ||||||
|  |     private static volatile RSAUtils INSTANCE; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 构造方法:初始化密钥存储路径(内部存储) | ||||||
|  |      */ | ||||||
|  |     private RSAUtils(Context context) { | ||||||
|  |         keyPath = context.getFilesDir() + File.separator + "keys" + File.separator; // 修正路径格式 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取单例实例 | ||||||
|  |      */ | ||||||
|  |     public static synchronized RSAUtils getInstance(Context context) { | ||||||
|  |         if (INSTANCE == null) { | ||||||
|  |             INSTANCE = new RSAUtils(context); | ||||||
|  |         } | ||||||
|  |         return INSTANCE; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 检查密钥文件是否存在 | ||||||
|  |      */ | ||||||
|  |     public boolean keysExist() { | ||||||
|  |         File publicKeyFile = new File(keyPath + PUBLIC_KEY_FILE); | ||||||
|  |         File privateKeyFile = new File(keyPath + PRIVATE_KEY_FILE); | ||||||
|  |         return publicKeyFile.exists() && privateKeyFile.exists(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 生成密钥对并保存到文件 | ||||||
|  |      */ | ||||||
|  |     public void generateAndSaveKeys() throws Exception { | ||||||
|  |         LogUtils.d(TAG, "开始生成 RSA 密钥对(2048位)"); | ||||||
|  |         KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_ALGORITHM); | ||||||
|  |         generator.initialize(KEY_SIZE); | ||||||
|  |         KeyPair keyPair = generator.generateKeyPair(); | ||||||
|  |  | ||||||
|  |         saveKey(PUBLIC_KEY_FILE, keyPair.getPublic().getEncoded()); | ||||||
|  |         saveKey(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded()); | ||||||
|  |         LogUtils.d(TAG, "密钥对生成并保存成功"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取或生成密钥对(线程安全) | ||||||
|  |      */ | ||||||
|  |     public KeyPair getOrGenerateKeys() throws Exception { | ||||||
|  |         if (!keysExist()) { | ||||||
|  |             synchronized (RSAUtils.class) { // 双重检查锁,避免多线程重复生成 | ||||||
|  |                 if (!keysExist()) { | ||||||
|  |                     generateAndSaveKeys(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return readKeysFromFile(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 从文件读取密钥对 | ||||||
|  |      */ | ||||||
|  |     private KeyPair readKeysFromFile() throws Exception { | ||||||
|  |         LogUtils.d(TAG, "读取密钥对文件"); | ||||||
|  |         try { | ||||||
|  |             byte[] publicKeyBytes = readFileToBytes(keyPath + PUBLIC_KEY_FILE); | ||||||
|  |             byte[] privateKeyBytes = readFileToBytes(keyPath + PRIVATE_KEY_FILE); | ||||||
|  |  | ||||||
|  |             X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes); | ||||||
|  |             PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes); | ||||||
|  |  | ||||||
|  |             KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM); | ||||||
|  |             PublicKey publicKey = factory.generatePublic(publicSpec); | ||||||
|  |             PrivateKey privateKey = factory.generatePrivate(privateSpec); | ||||||
|  |  | ||||||
|  |             return new KeyPair(publicKey, privateKey); | ||||||
|  |         } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { | ||||||
|  |             LogUtils.e(TAG, "密钥文件读取失败:" + e.getMessage()); | ||||||
|  |             throw new Exception("密钥文件损坏或格式错误", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 保存密钥到文件(通用方法) | ||||||
|  |      */ | ||||||
|  |     private void saveKey(String fileName, byte[] keyBytes) throws IOException { | ||||||
|  |         Objects.requireNonNull(keyBytes, "密钥字节数据不可为空"); | ||||||
|  |         File dir = new File(keyPath); | ||||||
|  |         if (!dir.exists() && !dir.mkdirs()) { | ||||||
|  |             throw new IOException("创建密钥目录失败:" + keyPath); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         FileOutputStream fos = null; | ||||||
|  |         try { | ||||||
|  |             fos = new FileOutputStream(keyPath + fileName); | ||||||
|  |             fos.write(keyBytes); | ||||||
|  |         } finally { | ||||||
|  |             if (fos != null) { | ||||||
|  |                 try { | ||||||
|  |                     fos.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 读取文件为字节数组(Java 7 兼容) | ||||||
|  |      */ | ||||||
|  |     private byte[] readFileToBytes(String filePath) throws IOException { | ||||||
|  |         File file = new File(filePath); | ||||||
|  |         if (!file.exists() || file.isDirectory()) { | ||||||
|  |             throw new IOException("文件不存在或为目录:" + filePath); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         FileInputStream fis = null; | ||||||
|  |         try { | ||||||
|  |             fis = new FileInputStream(file); | ||||||
|  |             byte[] data = new byte[(int) file.length()]; | ||||||
|  |             int bytesRead = fis.read(data); | ||||||
|  |             if (bytesRead != data.length) { | ||||||
|  |                 throw new IOException("文件读取不完整"); | ||||||
|  |             } | ||||||
|  |             return data; | ||||||
|  |         } finally { | ||||||
|  |             if (fis != null) { | ||||||
|  |                 try { | ||||||
|  |                     fis.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 公钥加密(带参数校验) | ||||||
|  |      */ | ||||||
|  |     public byte[] encryptWithPublicKey(String plainText, PublicKey publicKey) throws Exception { | ||||||
|  |         Objects.requireNonNull(plainText, "明文不可为空"); | ||||||
|  |         Objects.requireNonNull(publicKey, "公钥不可为空"); | ||||||
|  |  | ||||||
|  |         Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); | ||||||
|  |         cipher.init(Cipher.ENCRYPT_MODE, publicKey); | ||||||
|  |  | ||||||
|  |         // 检查数据长度是否超过 RSA 限制(2048位密钥最大明文为 214字节,PKCS1Padding) | ||||||
|  |         int maxPlainTextSize = cipher.getBlockSize() - 11; // PKCS1Padding 固定填充长度 | ||||||
|  |         if (plainText.getBytes("UTF-8").length > maxPlainTextSize) { | ||||||
|  |             throw new IllegalArgumentException("明文过长,最大支持 " + maxPlainTextSize + " 字节"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return cipher.doFinal(plainText.getBytes("UTF-8")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 私钥解密(带参数校验) | ||||||
|  |      */ | ||||||
|  |     public String decryptWithPrivateKey(byte[] encryptedData, PrivateKey privateKey) throws Exception { | ||||||
|  |         Objects.requireNonNull(encryptedData, "密文不可为空"); | ||||||
|  |         Objects.requireNonNull(privateKey, "私钥不可为空"); | ||||||
|  |  | ||||||
|  |         Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); | ||||||
|  |         cipher.init(Cipher.DECRYPT_MODE, privateKey); | ||||||
|  |         byte[] decryptedBytes = cipher.doFinal(encryptedData); | ||||||
|  |         return new String(decryptedBytes, "UTF-8"); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * 将 HTTP 传输的 Base64 字符串还原为加密字节数组(Java 7 兼容) | ||||||
|  |      * @param httpString Base64 字符串(非 null) | ||||||
|  |      * @return 加密字节数组 | ||||||
|  |      * @throws IllegalArgumentException 解码失败时抛出 | ||||||
|  |      */ | ||||||
|  |     public byte[] httpStringToEncryptBytes(String httpString) { | ||||||
|  |         Objects.requireNonNull(httpString, "HTTP 字符串不可为空"); | ||||||
|  |  | ||||||
|  |         // 计算缺失的填充符数量(Java 7 不支持 repeat(),手动拼接) | ||||||
|  |         int pad = httpString.length() % 4; | ||||||
|  |         StringBuilder paddedString = new StringBuilder(httpString); | ||||||
|  |         if (pad != 0) { | ||||||
|  |             for (int i = 0; i < pad; i++) { | ||||||
|  |                 paddedString.append('='); // 补全 '=' | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 使用 Base64 解码(Android 原生 Base64 类兼容 Java 7) | ||||||
|  |         return Base64.decode(paddedString.toString(), Base64.URL_SAFE); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,281 @@ | |||||||
|  | package cc.winboll.studio.libapputils.utils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @Author ZhanGSKen<zhangsken@qq.com> | ||||||
|  |  * @Date 2025/06/04 17:21 | ||||||
|  |  * @Describe 应用登录与接口工具 | ||||||
|  |  */ | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.os.Looper; | ||||||
|  | import cc.winboll.studio.libappbase.LogUtils; | ||||||
|  | import cc.winboll.studio.libapputils.models.ResponseData; | ||||||
|  | import cc.winboll.studio.libapputils.models.UserInfoModel; | ||||||
|  | import com.google.gson.Gson; | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.UnsupportedEncodingException; | ||||||
|  | import java.net.URLDecoder; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.security.KeyPair; | ||||||
|  | import java.security.PrivateKey; | ||||||
|  | import java.security.PublicKey; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | import okhttp3.Call; | ||||||
|  | import okhttp3.Callback; | ||||||
|  | import okhttp3.MediaType; | ||||||
|  | import okhttp3.OkHttpClient; | ||||||
|  | import okhttp3.Request; | ||||||
|  | import okhttp3.RequestBody; | ||||||
|  | import okhttp3.Response; | ||||||
|  |  | ||||||
|  | public class YunUtils { | ||||||
|  |     public static final String TAG = "YunUtils"; | ||||||
|  |     // 私有静态实例,类加载时创建 | ||||||
|  |     private static volatile YunUtils INSTANCE; | ||||||
|  |     Context mContext; | ||||||
|  |     UserInfoModel mUserInfoModel; | ||||||
|  |     String token = ""; | ||||||
|  |     String mDataFolderPath = ""; | ||||||
|  |     String mUserInfoModelPath = ""; | ||||||
|  |  | ||||||
|  |     private static final int CONNECT_TIMEOUT = 15; // 连接超时时间(秒) | ||||||
|  |     private static final int READ_TIMEOUT = 20;    // 读取超时时间(秒) | ||||||
|  |     private static volatile YunUtils instance; | ||||||
|  |     private OkHttpClient okHttpClient; | ||||||
|  |     private Handler mainHandler; // 主线程 Handler | ||||||
|  |  | ||||||
|  |     // 私有构造方法,防止外部实例化 | ||||||
|  |     private YunUtils(Context context) { | ||||||
|  |         LogUtils.d(TAG, "YunUtils"); | ||||||
|  |         mContext = context; | ||||||
|  |         mDataFolderPath = mContext.getExternalFilesDir(TAG).toString(); | ||||||
|  |         File fTest = new File(mDataFolderPath); | ||||||
|  |         if (!fTest.exists()) { | ||||||
|  |             fTest.mkdirs(); | ||||||
|  |         } | ||||||
|  |         mUserInfoModelPath = mDataFolderPath + File.separator + "UserInfoModel.rsajson"; | ||||||
|  |  | ||||||
|  |         okHttpClient = new OkHttpClient.Builder() | ||||||
|  |             .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) | ||||||
|  |             .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) | ||||||
|  |             .build(); | ||||||
|  |         mainHandler = new Handler(Looper.getMainLooper()); // 获取主线程 Looper | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 公共静态方法,返回唯一实例 | ||||||
|  |     public static synchronized YunUtils getInstance(Context context) { | ||||||
|  |         LogUtils.d(TAG, "getInstance"); | ||||||
|  |         if (INSTANCE == null) { | ||||||
|  |             INSTANCE = new YunUtils(context); | ||||||
|  |         } | ||||||
|  |         return INSTANCE; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void checkLoginStatus() { | ||||||
|  |         String token = getLocalToken(); | ||||||
|  |         LogUtils.d(TAG, String.format("checkLoginStatus token is %s", token)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     String getLocalToken() { | ||||||
|  |         UserInfoModel userInfoModel = loadUserInfoModel(); | ||||||
|  |         return (userInfoModel == null) ?"": userInfoModel.getToken(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void login(String host, UserInfoModel userInfoModel) { | ||||||
|  |         LogUtils.d(TAG, "login"); | ||||||
|  |  | ||||||
|  |         // 发送 POST 请求 | ||||||
|  |         String apiUrl = host + "/login/index.php"; | ||||||
|  |         // 序列化对象为JSON | ||||||
|  |         Gson gson = new Gson(); | ||||||
|  |         String jsonData = gson.toJson(userInfoModel); // 自动生成标准JSON | ||||||
|  |         //String jsonData = userInfoModel.toString(); | ||||||
|  |         LogUtils.d(TAG, "要发送的数据 : " + jsonData); | ||||||
|  |  | ||||||
|  |         sendPostRequest(apiUrl, jsonData, new OnResponseListener() { | ||||||
|  |                 // 成功回调(主线程) | ||||||
|  |                 @Override | ||||||
|  |                 public void onSuccess(String responseBody) { | ||||||
|  |                     LogUtils.d(TAG, "onSuccess"); | ||||||
|  |                     LogUtils.d(TAG, String.format("responseBody %s", responseBody)); | ||||||
|  |                     Gson gson = new Gson(); | ||||||
|  |                     ResponseData result = gson.fromJson(responseBody, ResponseData.class); // 转为 Result 实例 | ||||||
|  |                     if(result.getStatus().equals(ResponseData.STATUS_SUCCESS)) { | ||||||
|  |                          | ||||||
|  |                             UserInfoModel userInfoModel = result.getData(); | ||||||
|  |                             if (userInfoModel != null) { | ||||||
|  |                                 LogUtils.d(TAG, "收到网站 UserInfoModel"); | ||||||
|  |                                 String token = userInfoModel.getToken(); | ||||||
|  |                                 saveLocalToken(token); | ||||||
|  |                                 checkLoginStatus(); | ||||||
|  |                             } | ||||||
|  |                         | ||||||
|  |                     } else if(result.getStatus().equals(ResponseData.STATUS_ERROR)) { | ||||||
|  |                         try { | ||||||
|  |                             String decodedMessage = URLDecoder.decode(result.getMessage(), "UTF-8"); | ||||||
|  |                             LogUtils.d(TAG, "服务器返回信息: " + decodedMessage); | ||||||
|  |                         } catch (UnsupportedEncodingException e) { | ||||||
|  |                             LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // 失败回调(主线程) | ||||||
|  |                 @Override | ||||||
|  |                 public void onFailure(String errorMsg) { | ||||||
|  |                     LogUtils.d(TAG, errorMsg); | ||||||
|  |                     // 处理错误 | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void saveLocalToken(String token) { | ||||||
|  |         UserInfoModel userInfoModel = new UserInfoModel(); | ||||||
|  |         userInfoModel.setToken(token); | ||||||
|  |         saveUserInfoModel(userInfoModel); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     UserInfoModel loadUserInfoModel() { | ||||||
|  |         LogUtils.d(TAG, "loadUserInfoModel"); | ||||||
|  |         if (new File(mUserInfoModelPath).exists()) { | ||||||
|  |             try { | ||||||
|  |                 // 加载加密后的模型数据 | ||||||
|  |                 byte[] encryptedData = FileUtils.readByteArrayFromFile(mUserInfoModelPath); | ||||||
|  |                 // 加载 RSA 工具 | ||||||
|  |                 RSAUtils utils = RSAUtils.getInstance(mContext); | ||||||
|  |                 KeyPair keyPair = utils.getOrGenerateKeys(); | ||||||
|  |                 //PublicKey publicKey = keyPair.getPublic(); | ||||||
|  |                 PrivateKey privateKey = keyPair.getPrivate(); | ||||||
|  |                 // 私钥解密模型数据 | ||||||
|  |                 String szInfo = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate()); | ||||||
|  |                 LogUtils.d(TAG, String.format("szInfo %s", szInfo)); | ||||||
|  |                 mUserInfoModel = UserInfoModel.parseStringToBean(szInfo, UserInfoModel.class); | ||||||
|  |                 if (mUserInfoModel == null) { | ||||||
|  |                     LogUtils.d(TAG, "模型数据解析为空数据。"); | ||||||
|  |                 } | ||||||
|  |                 LogUtils.d(TAG, "UserInfoModel 解密加载结束。"); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             LogUtils.d(TAG, "云服务登录信息不存在。"); | ||||||
|  |             mUserInfoModel = null; | ||||||
|  |         } | ||||||
|  |         return mUserInfoModel; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     void saveUserInfoModel(UserInfoModel userInfoModel) { | ||||||
|  |         LogUtils.d(TAG, "saveUserInfoModel"); | ||||||
|  |         try { | ||||||
|  |             String szInfo = userInfoModel.toString(); | ||||||
|  |             LogUtils.d(TAG, "原始数据: " + szInfo); | ||||||
|  |  | ||||||
|  |             RSAUtils utils = RSAUtils.getInstance(mContext); | ||||||
|  |             KeyPair keyPair = utils.getOrGenerateKeys(); | ||||||
|  |             PublicKey publicKey = keyPair.getPublic(); | ||||||
|  |  | ||||||
|  |             // 公钥加密(传入字节数组,避免中间字符串转换) | ||||||
|  |             byte[] encryptedData = utils.encryptWithPublicKey(szInfo, publicKey); | ||||||
|  |  | ||||||
|  |             // 保存加密字节数组到文件(直接操作字节,无需转字符串) | ||||||
|  |             FileUtils.writeByteArrayToFile(encryptedData, mUserInfoModelPath); | ||||||
|  |             LogUtils.d(TAG, "加密数据已保存"); | ||||||
|  |  | ||||||
|  |             // 测试解密(仅调试用) | ||||||
|  |             String szInfo2 = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate()); | ||||||
|  |             LogUtils.d(TAG, "解密结果: " + szInfo2); | ||||||
|  |  | ||||||
|  |             mUserInfoModel = UserInfoModel.parseStringToBean(szInfo2, UserInfoModel.class); | ||||||
|  |             if (mUserInfoModel == null) { | ||||||
|  |                 LogUtils.d(TAG, "模型解析失败"); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             LogUtils.d(TAG, "加密/解密失败: " + e.getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 发送 POST 请求(JSON 数据) | ||||||
|  |     public void sendPostRequest(String url, String data, OnResponseListener listener) { | ||||||
|  |         RequestBody requestBody = RequestBody.create( | ||||||
|  |             MediaType.parse("application/json; charset=utf-8"), // 关键头信息 | ||||||
|  |             data.getBytes(StandardCharsets.UTF_8) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         Request request = new Request.Builder() | ||||||
|  |             .url(url) | ||||||
|  |             .post(requestBody) | ||||||
|  |             .addHeader("Content-Type", "application/json") // 显式添加头 | ||||||
|  |             .build(); | ||||||
|  |  | ||||||
|  |         executeRequest(request, listener); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 发送 GET 请求 | ||||||
|  |     public void sendGetRequest(String url, OnResponseListener listener) { | ||||||
|  |         Request request = new Request.Builder() | ||||||
|  |             .url(url) | ||||||
|  |             .get() | ||||||
|  |             .build(); | ||||||
|  |         executeRequest(request, listener); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 执行请求(子线程处理) | ||||||
|  |     private void executeRequest(final Request request, final OnResponseListener listener) { | ||||||
|  |         okHttpClient.newCall(request).enqueue(new Callback() { | ||||||
|  |                 // 响应成功(子线程) | ||||||
|  |                 @Override | ||||||
|  |                 public void onResponse(Call call, Response response) throws IOException { | ||||||
|  |                     try { | ||||||
|  |                         if (!response.isSuccessful()) { | ||||||
|  |                             postFailure(listener, "响应码错误:" + response.code()); | ||||||
|  |                             return; | ||||||
|  |                         } | ||||||
|  |                         String responseBody = response.body().string(); | ||||||
|  |                         postSuccess(listener, responseBody); | ||||||
|  |                     } catch (Exception e) { | ||||||
|  |                         postFailure(listener, "解析失败:" + e.getMessage()); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // 响应失败(子线程) | ||||||
|  |                 @Override | ||||||
|  |                 public void onFailure(Call call, IOException e) { | ||||||
|  |                     postFailure(listener, "网络失败:" + e.getMessage()); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 // 主线程回调(使用 Handler) | ||||||
|  |                 private void postSuccess(final OnResponseListener listener, final String msg) { | ||||||
|  |                     mainHandler.post(new Runnable() { | ||||||
|  |                             @Override | ||||||
|  |                             public void run() { | ||||||
|  |                                 listener.onSuccess(msg); | ||||||
|  |                             } | ||||||
|  |                         }); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 private void postFailure(final OnResponseListener listener, final String msg) { | ||||||
|  |                     mainHandler.post(new Runnable() { | ||||||
|  |                             @Override | ||||||
|  |                             public void run() { | ||||||
|  |                                 listener.onFailure(msg); | ||||||
|  |                             } | ||||||
|  |                         }); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public interface OnResponseListener { | ||||||
|  |         /** | ||||||
|  |          * 成功响应(主线程回调) | ||||||
|  |          * @param responseBody 响应体字符串 | ||||||
|  |          */ | ||||||
|  |         void onSuccess(String responseBody); | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 失败回调(包含错误信息) | ||||||
|  |          * @param errorMsg 错误描述 | ||||||
|  |          */ | ||||||
|  |         void onFailure(String errorMsg); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user