From 2f8f958a6a48e7fa696ece6e911232804b0f6f52 Mon Sep 17 00:00:00 2001 From: yao-1212 <59220794+yao-1212@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:07:58 +0800 Subject: [PATCH] =?UTF-8?q?2.17=20=E6=94=AF=E6=8C=81=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E7=9B=B8=E6=9C=BA=20=E6=94=AF=E6=8C=81=E5=8E=8B=E7=BC=A9+?= =?UTF-8?q?=E6=B0=B4=E5=8D=B0=E6=97=B6=E9=97=B4=EF=BC=8C=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E4=B8=8D=E9=BB=98=E8=AE=A4=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=97=B6=E9=97=B4=EF=BC=9B=E6=94=AF=E6=8C=81=E9=9C=8D=E5=B0=BC?= =?UTF-8?q?=E9=9F=A6=E5=B0=94eda52=E6=9C=8D=E5=8A=A1=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 3 +- .../business/activity/MainActivity.java | 1 + .../business/adapter/HoneywellAdapter.java | 9 +- .../business/utils/LocalAddressUtil.java | 261 ++++++++++++++++-- 4 files changed, 238 insertions(+), 36 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e58292b..864c620 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdk 28 targetSdk 28 versionCode 1 - versionName "2.16" + versionName "2.17" // 1.0 IDATA广播模式处理 // 1.1 霍尼韦尔的监听修改(扫描网站二维码跳出程序,监听失效,调整)、斑马PDA广播模式设置 @@ -52,6 +52,7 @@ android { // 2.14 适配 AIFUU 陈安良:陆军特色中心医院 // 2.15 添加文件选择和相机拍照功能 // 2.16 暴露了一个直接调用相机的方法,非http input调用会出现默认的弹出窗口进行选择(相机、文件) + // 2.17 支持原生相机,默认压缩;支持霍尼韦尔eda52服务扫描 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { abiFilters 'armeabi-v7a' diff --git a/app/src/main/java/chaoran/business/activity/MainActivity.java b/app/src/main/java/chaoran/business/activity/MainActivity.java index e3e1066..0f1669b 100644 --- a/app/src/main/java/chaoran/business/activity/MainActivity.java +++ b/app/src/main/java/chaoran/business/activity/MainActivity.java @@ -167,6 +167,7 @@ public class MainActivity extends AppCompatActivity implements ResultListener{ if ( "eda50p".equals(Build.MODEL.toLowerCase()) || "eda51".equals(Build.MODEL.toLowerCase()) + || "eda52".equals(Build.MODEL.toLowerCase()) || "tc26".equals(Build.MODEL.toLowerCase()) ) { // 走服务模式 diff --git a/app/src/main/java/chaoran/business/adapter/HoneywellAdapter.java b/app/src/main/java/chaoran/business/adapter/HoneywellAdapter.java index c6ddbbc..74285ed 100644 --- a/app/src/main/java/chaoran/business/adapter/HoneywellAdapter.java +++ b/app/src/main/java/chaoran/business/adapter/HoneywellAdapter.java @@ -98,14 +98,7 @@ public class HoneywellAdapter implements Adapter { public void openContinueScan(){ - if("eda50p".equals(Build.MODEL.toLowerCase())){ // 扫描正常 - intent = new Intent(context, ScanServiceEDA50P.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.getApplicationContext().startForegroundService(intent); - }else { - context.getApplicationContext().startService(intent); - } - }else if("eda51".equals(Build.MODEL.toLowerCase())){ // 扫描正常 + if("eda50p".equals(Build.MODEL.toLowerCase()) || "eda51".equals(Build.MODEL.toLowerCase()) || "eda52".equals(Build.MODEL.toLowerCase())){ // 扫描正常 intent = new Intent(context, ScanServiceEDA50P.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.getApplicationContext().startForegroundService(intent); diff --git a/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java b/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java index bb94cbf..bd9f8cc 100644 --- a/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java +++ b/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java @@ -9,6 +9,12 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.media.ExifInterface; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; @@ -34,11 +40,11 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.text.SimpleDateFormat; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.Locale; @@ -64,12 +70,18 @@ public class LocalAddressUtil { private static final String DEFAULT_CAMERA_CALLBACK = "onNativeCameraResult"; // Base64 settings for camera callback - private static final int CAMERA_IMAGE_MAX_DIMENSION = 1280; - private static final int CAMERA_IMAGE_JPEG_QUALITY = 80; - private static final String CAMERA_DATA_URL_PREFIX = "data:image/jpeg;base64,"; + private static final int CAMERA_IMAGE_DECODE_MAX_DIMENSION = 3200; + private static final int CAMERA_OUTPUT_JPEG_QUALITY = 90; + private static final int CAMERA_OUTPUT_JPEG_MIN_QUALITY = 55; + private static final int CAMERA_OUTPUT_BASE64_MAX_BYTES = 500 * 1024; + private static final int CAMERA_OUTPUT_IMAGE_MAX_BYTES = CAMERA_OUTPUT_BASE64_MAX_BYTES / 4 * 3; + private static final String CAMERA_RAW_DATA_URL_PREFIX = "data:image/jpeg;base64,"; + private static final String CAMERA_WATERMARK_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; private Uri nativeCameraPhotoUri; private String pendingCameraCallback = null; + private boolean pendingCameraCompress = true; + private boolean pendingCameraWatermark = false; public LocalAddressUtil(Context context, Activity activity, View view) { this.context = context; @@ -84,7 +96,7 @@ public class LocalAddressUtil { @SuppressLint("JavascriptInterface") @JavascriptInterface public void openNativeCamera() { - openNativeCamera(DEFAULT_CAMERA_CALLBACK); + openNativeCamera(DEFAULT_CAMERA_CALLBACK, true, false); } /** @@ -94,9 +106,23 @@ public class LocalAddressUtil { @SuppressLint("JavascriptInterface") @JavascriptInterface public void openNativeCamera(String callback) { + openNativeCamera(callback, true, false); + } + + /** + * 直接调用原生相机(自定义回调函数名:window[callback](base64OrDataUrl, error)) + * @param callback JS 回调函数名(不需要传 window. 前缀) + * @param needCompress 是否压缩,默认压缩到约 500KB base64 + * @param addWatermark 是否添加拍照时间水印 + */ + @SuppressLint("JavascriptInterface") + @JavascriptInterface + public void openNativeCamera(String callback, boolean needCompress, boolean addWatermark) { this.pendingCameraCallback = (callback == null || callback.trim().isEmpty()) ? DEFAULT_CAMERA_CALLBACK : callback.trim(); + this.pendingCameraCompress = needCompress; + this.pendingCameraWatermark = addWatermark; if (!checkCameraPermission()) { ActivityCompat.requestPermissions(activity, @@ -127,7 +153,7 @@ public class LocalAddressUtil { return false; } if (resultCode == Activity.RESULT_OK) { - String dataUrl = buildCameraPhotoDataUrl(nativeCameraPhotoUri); + String dataUrl = buildCameraPhotoDataUrl(nativeCameraPhotoUri, pendingCameraCompress, pendingCameraWatermark); if (dataUrl == null || dataUrl.trim().isEmpty()) { dispatchCameraResultToJs(null, "encode_failed"); } else { @@ -137,6 +163,8 @@ public class LocalAddressUtil { dispatchCameraResultToJs(null, "cancelled"); } nativeCameraPhotoUri = null; + pendingCameraCompress = true; + pendingCameraWatermark = false; return true; } @@ -150,7 +178,7 @@ public class LocalAddressUtil { boolean granted = grantResults != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; if (granted) { - openNativeCamera(pendingCameraCallback); + openNativeCamera(pendingCameraCallback, pendingCameraCompress, pendingCameraWatermark); } else { dispatchCameraResultToJs(null, "permission_denied"); } @@ -201,10 +229,39 @@ public class LocalAddressUtil { /** * 将拍照结果转为 dataUrl(base64) */ - private String buildCameraPhotoDataUrl(Uri uri) { + private String buildCameraPhotoDataUrl(Uri uri, boolean needCompress, boolean addWatermark) { if (uri == null) { return null; } + byte[] rawBytes; + try { + rawBytes = readBytesFromUri(uri); + if (rawBytes == null || rawBytes.length == 0) { + return null; + } + } catch (Exception e) { + Log.e(TAG, "读取原图失败", e); + return null; + } + + if (!addWatermark && (!needCompress || rawBytes.length <= CAMERA_OUTPUT_IMAGE_MAX_BYTES)) { + String base64 = Base64.encodeToString(rawBytes, Base64.NO_WRAP); + String dataUrl = CAMERA_RAW_DATA_URL_PREFIX + base64; + Log.i(TAG, "camera_base64_size rawBytes=" + rawBytes.length + + " rawKb=" + String.format(Locale.getDefault(), "%.2f", rawBytes.length / 1024f) + + " resultBytes=" + rawBytes.length + + " resultKb=" + String.format(Locale.getDefault(), "%.2f", rawBytes.length / 1024f) + + " base64Chars=" + base64.length() + + " base64Kb=" + String.format(Locale.getDefault(), "%.2f", base64.length() / 1024f) + + " dataUrlChars=" + dataUrl.length() + + " needCompress=" + needCompress + + " addWatermark=" + addWatermark + + " outputWidth=raw" + + " outputHeight=raw" + + " outputFormat=jpeg_raw"); + return dataUrl; + } + InputStream inputStream = null; try { inputStream = context.getContentResolver().openInputStream(uri); @@ -213,31 +270,139 @@ public class LocalAddressUtil { } BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(inputStream, null, options); - int srcW = options.outWidth; - int srcH = options.outHeight; - - safeClose(inputStream); - inputStream = context.getContentResolver().openInputStream(uri); - if (inputStream == null) { - return null; + if (needCompress) { + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + safeClose(inputStream); + inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + return null; + } + options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, CAMERA_IMAGE_DECODE_MAX_DIMENSION); + options.inJustDecodeBounds = false; } - - options.inSampleSize = calculateInSampleSize(srcW, srcH, CAMERA_IMAGE_MAX_DIMENSION); - options.inJustDecodeBounds = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); if (bitmap == null) { return null; } + int rotateDegree = readImageRotateDegree(uri); + if (rotateDegree != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(rotateDegree); + Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + if (rotatedBitmap != bitmap) { + bitmap.recycle(); + bitmap = rotatedBitmap; + } + } + + if (addWatermark) { + Bitmap.Config config = bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888; + if (!bitmap.isMutable()) { + Bitmap watermarkBitmap = bitmap.copy(config, true); + if (watermarkBitmap == null) { + return null; + } + if (watermarkBitmap != bitmap) { + bitmap.recycle(); + bitmap = watermarkBitmap; + } + } + Canvas canvas = new Canvas(bitmap); + String watermark = new SimpleDateFormat(CAMERA_WATERMARK_TIME_PATTERN, Locale.getDefault()).format(new Date()); + float textSize = Math.max(28f, bitmap.getWidth() / 16f); + float padding = Math.max(18f, textSize * 0.45f); + Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(Color.WHITE); + textPaint.setTextSize(textSize); + textPaint.setFakeBoldText(true); + textPaint.setShadowLayer(4f, 0f, 1f, 0x66000000); + Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); + Rect textBounds = new Rect(); + textPaint.getTextBounds(watermark, 0, watermark.length(), textBounds); + float left = padding; + float bottom = bitmap.getHeight() - padding; + float top = bottom - (fontMetrics.descent - fontMetrics.ascent) - padding * 1.6f; + float right = left + textBounds.width() + padding * 2f; + Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + bgPaint.setColor(0xB3000000); + canvas.drawRoundRect(left, top, right, bottom, padding, padding, bgPaint); + Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + borderPaint.setColor(0x66FFFFFF); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(Math.max(2f, textSize / 14f)); + canvas.drawRoundRect(left, top, right, bottom, padding, padding, borderPaint); + float textY = bottom - padding - fontMetrics.descent; + canvas.drawText(watermark, left + padding, textY, textPaint); + } + + byte[] bytes; + int quality = CAMERA_OUTPUT_JPEG_QUALITY; ByteArrayOutputStream baos = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, CAMERA_IMAGE_JPEG_QUALITY, baos); - byte[] bytes = baos.toByteArray(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); + bytes = baos.toByteArray(); + int outputWidth = bitmap.getWidth(); + int outputHeight = bitmap.getHeight(); + + if (needCompress && bytes.length > CAMERA_OUTPUT_IMAGE_MAX_BYTES) { + int guard = 0; + while (bytes.length > CAMERA_OUTPUT_IMAGE_MAX_BYTES && guard < 12) { + guard++; + while (bytes.length > CAMERA_OUTPUT_IMAGE_MAX_BYTES && quality > CAMERA_OUTPUT_JPEG_MIN_QUALITY) { + quality = Math.max(CAMERA_OUTPUT_JPEG_MIN_QUALITY, quality - 5); + baos.reset(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); + bytes = baos.toByteArray(); + } + if (bytes.length <= CAMERA_OUTPUT_IMAGE_MAX_BYTES) { + break; + } + + int nextWidth = Math.max(1, Math.round(bitmap.getWidth() * 0.85f)); + int nextHeight = Math.max(1, Math.round(bitmap.getHeight() * 0.85f)); + if (nextWidth == bitmap.getWidth() && nextHeight == bitmap.getHeight()) { + break; + } + + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, nextWidth, nextHeight, true); + if (scaledBitmap != bitmap) { + bitmap.recycle(); + bitmap = scaledBitmap; + } + outputWidth = bitmap.getWidth(); + outputHeight = bitmap.getHeight(); + quality = CAMERA_OUTPUT_JPEG_QUALITY; + baos.reset(); + bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); + bytes = baos.toByteArray(); + } + } + + bitmap.recycle(); + if (bytes == null || bytes.length == 0) { + return null; + } String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP); - return CAMERA_DATA_URL_PREFIX + base64; + String dataUrl = CAMERA_RAW_DATA_URL_PREFIX + base64; + Log.i(TAG, "camera_base64_size rawBytes=" + rawBytes.length + + " rawKb=" + String.format(Locale.getDefault(), "%.2f", rawBytes.length / 1024f) + + " resultBytes=" + bytes.length + + " resultKb=" + String.format(Locale.getDefault(), "%.2f", bytes.length / 1024f) + + " base64Chars=" + base64.length() + + " base64Kb=" + String.format(Locale.getDefault(), "%.2f", base64.length() / 1024f) + + " dataUrlChars=" + dataUrl.length() + + " needCompress=" + needCompress + + " addWatermark=" + addWatermark + + " outputWidth=" + outputWidth + + " outputHeight=" + outputHeight + + " jpegQuality=" + quality + + " targetBytes=" + (needCompress ? CAMERA_OUTPUT_IMAGE_MAX_BYTES : -1) + + " outputFormat=jpeg"); + return dataUrl; } catch (Exception e) { Log.e(TAG, "图片转base64失败", e); return null; @@ -246,14 +411,56 @@ public class LocalAddressUtil { } } + private byte[] readBytesFromUri(Uri uri) throws IOException { + InputStream inputStream = null; + try { + inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + return baos.toByteArray(); + } finally { + safeClose(inputStream); + } + } + + private int readImageRotateDegree(Uri uri) { + InputStream inputStream = null; + try { + inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + return 0; + } + ExifInterface exifInterface = new ExifInterface(inputStream); + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + if (orientation == ExifInterface.ORIENTATION_ROTATE_90) { + return 90; + } + if (orientation == ExifInterface.ORIENTATION_ROTATE_180) { + return 180; + } + if (orientation == ExifInterface.ORIENTATION_ROTATE_270) { + return 270; + } + } catch (Exception e) { + Log.w(TAG, "读取图片方向失败", e); + } finally { + safeClose(inputStream); + } + return 0; + } + private int calculateInSampleSize(int srcW, int srcH, int maxDim) { - if (srcW <= 0 || srcH <= 0) { + if (srcW <= 0 || srcH <= 0 || maxDim <= 0) { return 1; } int maxSrc = Math.max(srcW, srcH); - if (maxSrc <= maxDim) { - return 1; - } int inSampleSize = 1; while (maxSrc / inSampleSize > maxDim) { inSampleSize *= 2;