diff --git a/app/build.gradle b/app/build.gradle index 4a5fe01..e58292b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdk 28 targetSdk 28 versionCode 1 - versionName "2.15" + versionName "2.16" // 1.0 IDATA广播模式处理 // 1.1 霍尼韦尔的监听修改(扫描网站二维码跳出程序,监听失效,调整)、斑马PDA广播模式设置 @@ -51,6 +51,7 @@ android { // 瑞芯适配器 接入 新的型号,使用的是 ttyS8;而不是ttyS1;并且只有一个接口。 // 2.14 适配 AIFUU 陈安良:陆军特色中心医院 // 2.15 添加文件选择和相机拍照功能 + // 2.16 暴露了一个直接调用相机的方法,非http input调用会出现默认的弹出窗口进行选择(相机、文件) 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 972e78a..e3e1066 100644 --- a/app/src/main/java/chaoran/business/activity/MainActivity.java +++ b/app/src/main/java/chaoran/business/activity/MainActivity.java @@ -80,6 +80,7 @@ public class MainActivity extends AppCompatActivity implements ResultListener{ private ProgressBar progressBar; private ActionBar actionBar; private FileChooserHelper fileChooserHelper; + private LocalAddressUtil localAddressUtil; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -236,7 +237,8 @@ public class MainActivity extends AppCompatActivity implements ResultListener{ webView.addJavascriptInterface(settingEngine, "NetworkSettingEngine"); //重新加载页面 webView.addJavascriptInterface(this, "View"); - webView.addJavascriptInterface(new LocalAddressUtil(this, this, webView), "Localpda"); + localAddressUtil = new LocalAddressUtil(this, this, webView); + webView.addJavascriptInterface(localAddressUtil, "Localpda"); webView.loadUrl(url()); // StatusBarUtil.transparencyBar( this); // 设置全部透明,需要在页面设置一个参数进行布局的样式跳转,不同的手机端,状态栏高度不一样(apk设置状态栏高度无效,这个是安卓9的一个bug),所以在此采取隐藏状态栏 if (hideBar == 1) { @@ -583,7 +585,8 @@ public class MainActivity extends AppCompatActivity implements ResultListener{ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (!fileChooserHelper.onActivityResult(requestCode, resultCode, data)) { + if (!fileChooserHelper.onActivityResult(requestCode, resultCode, data) + && (localAddressUtil == null || !localAddressUtil.onActivityResult(requestCode, resultCode, data))) { // 如果不是文件选择器的结果,可以在这里处理其他结果 } } @@ -591,7 +594,8 @@ public class MainActivity extends AppCompatActivity implements ResultListener{ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (!fileChooserHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)) { + if (!fileChooserHelper.onRequestPermissionsResult(requestCode, permissions, grantResults) + && (localAddressUtil == null || !localAddressUtil.onRequestPermissionsResult(requestCode, permissions, grantResults))) { // 如果不是文件选择器的权限请求,可以在这里处理其他权限请求 } } diff --git a/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java b/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java index 70fdfc0..bb94cbf 100644 --- a/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java +++ b/app/src/main/java/chaoran/business/utils/LocalAddressUtil.java @@ -1,27 +1,47 @@ package chaoran.business.utils; -import static androidx.core.content.ContextCompat.getSystemService; - import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; +import android.os.Environment; import android.provider.Settings; +import android.provider.MediaStore; +import android.util.Base64; import android.util.Log; import android.view.Surface; import android.view.View; import android.view.WindowManager; import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; + +import org.json.JSONObject; + +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.util.Date; import java.util.Enumeration; +import java.util.Locale; import java.util.Random; import chaoran.business.BuildConfig; @@ -38,6 +58,19 @@ public class LocalAddressUtil { private int[] heights; + private static final String TAG = "LocalAddressUtil"; + private static final int REQUEST_CODE_NATIVE_CAMERA = 31002; + private static final int REQUEST_CODE_PERMISSION_CAMERA = 31001; + 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 Uri nativeCameraPhotoUri; + private String pendingCameraCallback = null; + public LocalAddressUtil(Context context, Activity activity, View view) { this.context = context; this.activity = activity; @@ -45,6 +78,221 @@ public class LocalAddressUtil { this.heights = StatusBarUtil.getStatusBarHeight(context); } + /** + * 直接调用原生相机(默认回调:window.onNativeCameraResult(base64OrDataUrl, error)) + */ + @SuppressLint("JavascriptInterface") + @JavascriptInterface + public void openNativeCamera() { + openNativeCamera(DEFAULT_CAMERA_CALLBACK); + } + + /** + * 直接调用原生相机(自定义回调函数名:window[callback](base64OrDataUrl, error)) + * @param callback JS 回调函数名(不需要传 window. 前缀) + */ + @SuppressLint("JavascriptInterface") + @JavascriptInterface + public void openNativeCamera(String callback) { + this.pendingCameraCallback = (callback == null || callback.trim().isEmpty()) + ? DEFAULT_CAMERA_CALLBACK + : callback.trim(); + + if (!checkCameraPermission()) { + ActivityCompat.requestPermissions(activity, + new String[]{android.Manifest.permission.CAMERA}, + REQUEST_CODE_PERMISSION_CAMERA); + return; + } + + Intent cameraIntent = createNativeCameraIntent(); + if (cameraIntent == null) { + dispatchCameraResultToJs(null, "create_intent_failed"); + return; + } + + try { + activity.startActivityForResult(cameraIntent, REQUEST_CODE_NATIVE_CAMERA); + } catch (Exception e) { + Log.e(TAG, "启动相机失败", e); + dispatchCameraResultToJs(null, "start_failed"); + } + } + + /** + * 由宿主 Activity 转发调用 + */ + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_CODE_NATIVE_CAMERA) { + return false; + } + if (resultCode == Activity.RESULT_OK) { + String dataUrl = buildCameraPhotoDataUrl(nativeCameraPhotoUri); + if (dataUrl == null || dataUrl.trim().isEmpty()) { + dispatchCameraResultToJs(null, "encode_failed"); + } else { + dispatchCameraResultToJs(dataUrl, null); + } + } else { + dispatchCameraResultToJs(null, "cancelled"); + } + nativeCameraPhotoUri = null; + return true; + } + + /** + * 由宿主 Activity 转发调用 + */ + public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode != REQUEST_CODE_PERMISSION_CAMERA) { + return false; + } + boolean granted = grantResults != null && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED; + if (granted) { + openNativeCamera(pendingCameraCallback); + } else { + dispatchCameraResultToJs(null, "permission_denied"); + } + return true; + } + + private boolean checkCameraPermission() { + return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED; + } + + private Intent createNativeCameraIntent() { + Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (cameraIntent.resolveActivity(activity.getPackageManager()) == null) { + return null; + } + File photoFile = createImageFile(); + if (photoFile == null) { + return null; + } + nativeCameraPhotoUri = FileProvider.getUriForFile( + activity, + activity.getPackageName() + ".fileprovider", + photoFile + ); + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, nativeCameraPhotoUri); + cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + return cameraIntent; + } + + private File createImageFile() { + try { + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (storageDir != null && !storageDir.exists()) { + //noinspection ResultOfMethodCallIgnored + storageDir.mkdirs(); + } + return File.createTempFile(imageFileName, ".jpg", storageDir); + } catch (IOException e) { + Log.e(TAG, "创建图片文件失败", e); + return null; + } + } + + /** + * 将拍照结果转为 dataUrl(base64) + */ + private String buildCameraPhotoDataUrl(Uri uri) { + if (uri == null) { + return null; + } + InputStream inputStream = null; + try { + inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + return null; + } + + 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; + } + + options.inSampleSize = calculateInSampleSize(srcW, srcH, CAMERA_IMAGE_MAX_DIMENSION); + options.inJustDecodeBounds = false; + + Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); + if (bitmap == null) { + return null; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, CAMERA_IMAGE_JPEG_QUALITY, baos); + byte[] bytes = baos.toByteArray(); + + String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP); + return CAMERA_DATA_URL_PREFIX + base64; + } catch (Exception e) { + Log.e(TAG, "图片转base64失败", e); + return null; + } finally { + safeClose(inputStream); + } + } + + private int calculateInSampleSize(int srcW, int srcH, int maxDim) { + if (srcW <= 0 || srcH <= 0) { + return 1; + } + int maxSrc = Math.max(srcW, srcH); + if (maxSrc <= maxDim) { + return 1; + } + int inSampleSize = 1; + while (maxSrc / inSampleSize > maxDim) { + inSampleSize *= 2; + } + return Math.max(1, inSampleSize); + } + + private void safeClose(InputStream is) { + try { + if (is != null) { + is.close(); + } + } catch (Exception ignored) { + } + } + + private void dispatchCameraResultToJs(String dataUrl, String error) { + if (!(view instanceof WebView)) { + return; + } + WebView webView = (WebView) view; + String cb = (pendingCameraCallback == null || pendingCameraCallback.trim().isEmpty()) + ? DEFAULT_CAMERA_CALLBACK + : pendingCameraCallback.trim(); + + String js = "try{" + + "var cb=window[" + JSONObject.quote(cb) + "];" + + "if(typeof cb==='function'){cb(" + JSONObject.quote(dataUrl) + "," + JSONObject.quote(error) + ");}" + + "}catch(e){}"; + + webView.post(() -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, null); + } else { + webView.loadUrl("javascript:" + js); + } + }); + } + @SuppressLint("JavascriptInterface") @JavascriptInterface public String getIpAddress() {