@ -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 = 128 0;
private static final int CAMERA_IMAGE _JPEG_QUALITY = 8 0;
private static final Str ing CAMERA_DATA_URL_PREFIX = " data:image/jpeg;base64, " ;
private static final int CAMERA_IMAGE_DECODE_ MAX_DIMENSION = 320 0;
private static final int CAMERA_OUTPUT _JPEG_QUALITY = 9 0;
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 ( ) ;
if ( needCompress ) {
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 ;
}
options . inSampleSize = calculateInSampleSize ( srcW , srcH , CAMERA_IMAGE_MAX_DIMENSION ) ;
options . inSampleSize = calculateInSampleSize ( options . outWidth , options . outHeight , CAMERA_IMAGE_DECODE_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 ;