feat: 添加文件选择和相机拍照功能
主要功能: 1. 支持WebView中选择文件(任意类型) 2. 支持选择图片(相册+相机) 3. 支持直接调用相机拍照 4. 自动处理相机和存储权限请求 新增文件: - FileChooserHelper.java - 文件选择和相机功能核心工具类 - CameraHelper.java - 直接调用相机的JavaScript接口 - FileTestActivity.java - 文件选择测试Activity - file_paths.xml - FileProvider配置文件 - file_test.html - 功能测试页面(ES5语法兼容老版本) - INTEGRATION_GUIDE.md - 接入指南文档 - FILE_CHOOSER_USAGE.md - 详细使用文档 修改文件: - AndroidManifest.xml - 添加相机和存储权限,注册FileProvider和FileTestActivity - MainActivity.java - 集成FileChooserHelper,添加WebChromeClient支持文件选择 - menu_main.xml - 添加"文件测试"菜单项 - strings.xml - 添加相关字符串资源 技术特性: - 支持Android 9+(API 28+) - 适配Android 10+分区存储 - 适配Android 13+新媒体权限 - 权限授予后自动重新打开文件选择器 - 使用ES5语法兼容老版本WebView - 支持FileProvider安全文件共享 使用方式: 在WebView加载的HTML页面中使用标准input标签: <input type="file" accept="image/*" capture="environment"> 版本:2.15
This commit is contained in:
@ -14,6 +14,12 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="com.honeywell.decode.permission.DECODE" />
|
||||
<uses-permission android:name="android.permission.STOP_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@ -32,6 +38,9 @@
|
||||
<activity
|
||||
android:name=".activity.VoiceSettingActivity"
|
||||
android:label="@string/title_activity_setting_voice" />
|
||||
<activity
|
||||
android:name=".activity.FileTestActivity"
|
||||
android:label="@string/title_activity_file_test" />
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
@ -53,6 +62,16 @@
|
||||
<action android:name="com.example.chaoran.ScanServiceZEBRA"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
175
app/src/main/assets/demo/file_test.html
Normal file
175
app/src/main/assets/demo/file_test.html
Normal file
@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>文件选择测试</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; max-width: 600px; margin: 0 auto; }
|
||||
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||||
h2 { color: #333; margin-top: 0; }
|
||||
input[type="file"] { display: block; margin: 10px 0; padding: 10px; width: 100%; box-sizing: border-box; }
|
||||
.preview { margin-top: 15px; max-width: 100%; }
|
||||
.preview img { max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 5px; }
|
||||
.info { margin-top: 10px; padding: 10px; background-color: #f5f5f5; border-radius: 5px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>文件选择和相机测试</h1>
|
||||
|
||||
<!-- 测试1:选择任意文件 -->
|
||||
<div class="section">
|
||||
<h2>1. 选择任意文件</h2>
|
||||
<input type="file" id="fileInput" onchange="handleFileSelect(this)">
|
||||
<div id="fileInfo" class="info" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 测试2:选择图片(支持相机和文件) -->
|
||||
<div class="section">
|
||||
<h2>2. 选择图片(支持相机)</h2>
|
||||
<!-- accept="image/*": 只接受图片, capture="environment": 启用相机 -->
|
||||
<input type="file" id="imageInput" accept="image/*" capture="environment" onchange="handleImageSelect(this)">
|
||||
<div id="imageInfo" class="info" style="display:none;"></div>
|
||||
<div id="imagePreview" class="preview"></div>
|
||||
</div>
|
||||
|
||||
<!-- 测试3:直接打开相机 -->
|
||||
<div class="section">
|
||||
<h2>3. 仅使用相机拍照</h2>
|
||||
<!-- 通过JavaScript接口直接调用相机,不显示选择器 -->
|
||||
<button onclick="openCameraDirectly()" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">打开相机</button>
|
||||
<div id="cameraInfo" class="info" style="display:none;"></div>
|
||||
<div id="cameraPreview" class="preview"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 处理文件选择 - 显示文件信息
|
||||
*/
|
||||
function handleFileSelect(input) {
|
||||
var file = input.files[0]; // 获取选择的文件
|
||||
var infoDiv = document.getElementById('fileInfo'); // 获取信息显示区域
|
||||
|
||||
if (file) {
|
||||
infoDiv.style.display = 'block'; // 显示信息区域
|
||||
infoDiv.innerHTML = '<strong>文件信息:</strong><br>文件名: ' + file.name + '<br>文件大小: ' + formatFileSize(file.size) + '<br>文件类型: ' + (file.type || '未知');
|
||||
} else {
|
||||
infoDiv.style.display = 'none'; // 隐藏信息区域
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片选择 - 显示信息和预览
|
||||
*/
|
||||
function handleImageSelect(input) {
|
||||
var file = input.files[0]; // 获取选择的文件
|
||||
var infoDiv = document.getElementById('imageInfo');
|
||||
var previewDiv = document.getElementById('imagePreview');
|
||||
|
||||
if (file) {
|
||||
infoDiv.style.display = 'block';
|
||||
infoDiv.innerHTML = '<strong>图片信息:</strong><br>文件名: ' + file.name + '<br>文件大小: ' + formatFileSize(file.size) + '<br>文件类型: ' + file.type;
|
||||
|
||||
// 使用FileReader读取图片并显示预览
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
// e.target.result 是图片的Base64数据
|
||||
previewDiv.innerHTML = '<img src="' + e.target.result + '" alt="图片预览">';
|
||||
};
|
||||
reader.readAsDataURL(file); // 读取为DataURL格式
|
||||
} else {
|
||||
infoDiv.style.display = 'none';
|
||||
previewDiv.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接打开相机 - 通过Android接口
|
||||
*/
|
||||
function openCameraDirectly() {
|
||||
// CameraHelper是Android注入的JavaScript接口
|
||||
if (typeof CameraHelper !== 'undefined') {
|
||||
CameraHelper.openCamera(); // 调用Android方法打开相机
|
||||
} else {
|
||||
alert('相机功能不可用');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 相机拍照结果回调 - 由Android调用
|
||||
* @param uri - 照片URI
|
||||
* @param path - 照片路径
|
||||
*/
|
||||
function onCameraResult(uri, path) {
|
||||
var infoDiv = document.getElementById('cameraInfo');
|
||||
var previewDiv = document.getElementById('cameraPreview');
|
||||
|
||||
if (uri && path) {
|
||||
// 拍照成功
|
||||
infoDiv.style.display = 'block';
|
||||
infoDiv.innerHTML = '<strong>拍照成功:</strong><br>URI: ' + uri + '<br>路径: ' + path;
|
||||
previewDiv.innerHTML = '<img src="' + uri + '" alt="照片预览">';
|
||||
} else {
|
||||
// 拍照取消
|
||||
infoDiv.style.display = 'block';
|
||||
infoDiv.innerHTML = '<strong>拍照已取消</strong>';
|
||||
previewDiv.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小 - 字节转换为KB/MB/GB
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
var k = 1024; // 换算基数
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB']; // 单位数组
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k)); // 计算单位索引
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; // 计算并返回
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!--
|
||||
其他地方接入
|
||||
<input type="file" accept="image/*" capture="environment" onchange="handlePhoto(this)">
|
||||
// 处理照片选择
|
||||
function handlePhoto(input) {
|
||||
var file = input.files[0];
|
||||
|
||||
if (file) {
|
||||
// 读取文件
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var base64Data = e.target.result;
|
||||
|
||||
// 显示预览
|
||||
document.getElementById('preview').innerHTML =
|
||||
'<img src="' + base64Data + '" style="max-width: 100%;">';
|
||||
|
||||
// 上传到服务器
|
||||
uploadPhoto(base64Data);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传照片
|
||||
function uploadPhoto(base64Data) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/upload', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
console.log('上传成功');
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify({
|
||||
image: base64Data
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
-->
|
||||
@ -0,0 +1,113 @@
|
||||
package chaoran.business.activity;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import chaoran.business.R;
|
||||
import chaoran.business.utils.FileChooserHelper;
|
||||
import chaoran.business.utils.CameraHelper;
|
||||
|
||||
/**
|
||||
* 文件选择测试Activity
|
||||
*/
|
||||
public class FileTestActivity extends AppCompatActivity {
|
||||
|
||||
private WebView webView;
|
||||
private FileChooserHelper fileChooserHelper;
|
||||
private CameraHelper cameraHelper;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_file_test);
|
||||
|
||||
fileChooserHelper = new FileChooserHelper(this);
|
||||
|
||||
webView = findViewById(R.id.webViewFileTest);
|
||||
cameraHelper = new CameraHelper(this, webView);
|
||||
initWebView();
|
||||
|
||||
// 加载测试页面
|
||||
webView.loadUrl("file:///android_asset/demo/file_test.html");
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private void initWebView() {
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
|
||||
// 添加JavaScript接口
|
||||
webView.addJavascriptInterface(cameraHelper, "CameraHelper");
|
||||
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
|
||||
FileChooserParams fileChooserParams) {
|
||||
// 检查是否接受图片
|
||||
String[] acceptTypes = fileChooserParams.getAcceptTypes();
|
||||
boolean acceptImage = false;
|
||||
boolean acceptCamera = false;
|
||||
boolean cameraOnly = false;
|
||||
|
||||
if (acceptTypes != null && acceptTypes.length > 0) {
|
||||
for (String type : acceptTypes) {
|
||||
if (type.contains("image")) {
|
||||
acceptImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查capture模式
|
||||
if (fileChooserParams.isCaptureEnabled()) {
|
||||
acceptCamera = true;
|
||||
|
||||
// 注意:Android WebView中,capture="user"或capture="environment"都会返回true
|
||||
// 我们通过简单的方式判断:如果是图片且启用capture,默认显示选择器
|
||||
// 只有在特定情况下才直接打开相机
|
||||
|
||||
// 这里我们不设置cameraOnly=true,让所有capture都显示选择器
|
||||
// 如果需要仅相机模式,可以通过其他方式(如URL参数)来控制
|
||||
}
|
||||
|
||||
fileChooserHelper.openFileChooser(filePathCallback, acceptImage, acceptCamera, cameraOnly);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (!fileChooserHelper.onActivityResult(requestCode, resultCode, data)) {
|
||||
cameraHelper.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
fileChooserHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,7 @@ import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
@ -58,6 +59,7 @@ import chaoran.business.service.ScanServiceZEBRA;
|
||||
import chaoran.business.utils.DataCleanManager;
|
||||
import chaoran.business.utils.LocalAddressUtil;
|
||||
import chaoran.business.utils.StatusBarUtil;
|
||||
import chaoran.business.utils.FileChooserHelper;
|
||||
|
||||
/**
|
||||
* 流程:联网认证设备型号,验证通过,查找设备品牌进行调用驱动操作
|
||||
@ -77,6 +79,7 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
|
||||
private SettingEngine settingEngine;
|
||||
private ProgressBar progressBar;
|
||||
private ActionBar actionBar;
|
||||
private FileChooserHelper fileChooserHelper;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@ -215,9 +218,11 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
|
||||
actionBar = getSupportActionBar();
|
||||
voiceEngine = new TekVoiceEngine(this);
|
||||
settingEngine = new NetworkSettingEngine(this);
|
||||
fileChooserHelper = new FileChooserHelper(this);
|
||||
webView = findViewById(R.id.webView);
|
||||
progressBar = findViewById(R.id.loading);
|
||||
webView.setWebViewClient(disposeView());
|
||||
webView.setWebChromeClient(createWebChromeClient());
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
|
||||
@ -241,6 +246,48 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
|
||||
|
||||
private boolean isPageFinished = false; // 新增标志位
|
||||
|
||||
/**
|
||||
* 创建WebChromeClient以支持文件选择
|
||||
*/
|
||||
private WebChromeClient createWebChromeClient() {
|
||||
return new WebChromeClient() {
|
||||
// For Android 5.0+
|
||||
@Override
|
||||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
|
||||
FileChooserParams fileChooserParams) {
|
||||
// 检查是否接受图片
|
||||
String[] acceptTypes = fileChooserParams.getAcceptTypes();
|
||||
boolean acceptImage = false;
|
||||
boolean acceptCamera = false;
|
||||
boolean cameraOnly = false;
|
||||
|
||||
if (acceptTypes != null && acceptTypes.length > 0) {
|
||||
for (String type : acceptTypes) {
|
||||
if (type.contains("image")) {
|
||||
acceptImage = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查capture模式
|
||||
if (fileChooserParams.isCaptureEnabled()) {
|
||||
acceptCamera = true;
|
||||
|
||||
// 注意:Android WebView中,capture="user"或capture="environment"都会返回true
|
||||
// 我们通过简单的方式判断:如果是图片且启用capture,默认显示选择器
|
||||
// 只有在特定情况下才直接打开相机
|
||||
|
||||
// 这里我们不设置cameraOnly=true,让所有capture都显示选择器
|
||||
// 如果需要仅相机模式,可以通过其他方式(如URL参数)来控制
|
||||
}
|
||||
|
||||
fileChooserHelper.openFileChooser(filePathCallback, acceptImage, acceptCamera, cameraOnly);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//配置客户端
|
||||
private WebViewClient disposeView() {
|
||||
return new WebViewClient() {
|
||||
@ -391,6 +438,9 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
|
||||
case R.id.action_setting_voice:
|
||||
startActivity(new Intent(this, VoiceSettingActivity.class));
|
||||
break;
|
||||
case R.id.action_file_test:
|
||||
startActivity(new Intent(this, FileTestActivity.class));
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@ -529,4 +579,20 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
|
||||
.show();
|
||||
// 不调用 super.onBackPressed(),避免直接退出
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (!fileChooserHelper.onActivityResult(requestCode, resultCode, data)) {
|
||||
// 如果不是文件选择器的结果,可以在这里处理其他结果
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (!fileChooserHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
|
||||
// 如果不是文件选择器的权限请求,可以在这里处理其他权限请求
|
||||
}
|
||||
}
|
||||
}
|
||||
97
app/src/main/java/chaoran/business/utils/CameraHelper.java
Normal file
97
app/src/main/java/chaoran/business/utils/CameraHelper.java
Normal file
@ -0,0 +1,97 @@
|
||||
package chaoran.business.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 相机辅助类 - 通过JavaScript接口直接调用相机
|
||||
*/
|
||||
public class CameraHelper {
|
||||
|
||||
private static final int REQUEST_CODE_CAMERA_DIRECT = 1003;
|
||||
private Activity activity;
|
||||
private WebView webView;
|
||||
private Uri cameraPhotoUri;
|
||||
private String currentPhotoPath;
|
||||
|
||||
public CameraHelper(Activity activity, WebView webView) {
|
||||
this.activity = activity;
|
||||
this.webView = webView;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接打开相机拍照
|
||||
*/
|
||||
@JavascriptInterface
|
||||
public void openCamera() {
|
||||
activity.runOnUiThread(() -> {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
if (cameraIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
File photoFile = createImageFile();
|
||||
if (photoFile != null) {
|
||||
currentPhotoPath = photoFile.getAbsolutePath();
|
||||
cameraPhotoUri = FileProvider.getUriForFile(
|
||||
activity,
|
||||
activity.getPackageName() + ".fileprovider",
|
||||
photoFile
|
||||
);
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraPhotoUri);
|
||||
cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
activity.startActivityForResult(cameraIntent, REQUEST_CODE_CAMERA_DIRECT);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片文件
|
||||
*/
|
||||
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()) {
|
||||
storageDir.mkdirs();
|
||||
}
|
||||
return File.createTempFile(imageFileName, ".jpg", storageDir);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Activity结果
|
||||
*/
|
||||
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQUEST_CODE_CAMERA_DIRECT) {
|
||||
if (resultCode == Activity.RESULT_OK && cameraPhotoUri != null) {
|
||||
// 通知JavaScript照片已拍摄
|
||||
String jsCode = "javascript:onCameraResult('" + cameraPhotoUri.toString() + "', '" + currentPhotoPath + "')";
|
||||
webView.post(() -> webView.loadUrl(jsCode));
|
||||
} else {
|
||||
// 取消拍照
|
||||
String jsCode = "javascript:onCameraResult(null, null)";
|
||||
webView.post(() -> webView.loadUrl(jsCode));
|
||||
}
|
||||
cameraPhotoUri = null;
|
||||
currentPhotoPath = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
378
app/src/main/java/chaoran/business/utils/FileChooserHelper.java
Normal file
378
app/src/main/java/chaoran/business/utils/FileChooserHelper.java
Normal file
@ -0,0 +1,378 @@
|
||||
package chaoran.business.utils;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 文件选择和相机拍照辅助类
|
||||
*/
|
||||
public class FileChooserHelper {
|
||||
|
||||
private static final String TAG = "FileChooserHelper";
|
||||
private static final int REQUEST_CODE_CAMERA = 1001;
|
||||
private static final int REQUEST_CODE_FILE_CHOOSER = 1002;
|
||||
private static final int REQUEST_CODE_PERMISSION_CAMERA = 2001;
|
||||
private static final int REQUEST_CODE_PERMISSION_STORAGE = 2002;
|
||||
|
||||
private Activity activity;
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
private Uri cameraPhotoUri;
|
||||
private boolean pendingAcceptImage = false;
|
||||
private boolean pendingAcceptCamera = false;
|
||||
private boolean pendingCameraOnly = false;
|
||||
|
||||
public FileChooserHelper(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件选择器(支持相机和图片选择)
|
||||
*/
|
||||
public void openFileChooser(ValueCallback<Uri[]> callback, boolean acceptImage, boolean acceptCamera) {
|
||||
openFileChooser(callback, acceptImage, acceptCamera, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件选择器(支持相机和图片选择)
|
||||
* @param callback 文件选择回调
|
||||
* @param acceptImage 是否只接受图片
|
||||
* @param acceptCamera 是否支持相机
|
||||
* @param cameraOnly 是否仅使用相机(不显示文件选择器)
|
||||
*/
|
||||
public void openFileChooser(ValueCallback<Uri[]> callback, boolean acceptImage, boolean acceptCamera, boolean cameraOnly) {
|
||||
this.filePathCallback = callback;
|
||||
this.pendingAcceptImage = acceptImage;
|
||||
this.pendingAcceptCamera = acceptCamera;
|
||||
this.pendingCameraOnly = cameraOnly;
|
||||
|
||||
// 检查权限
|
||||
if (acceptCamera && !checkCameraPermission()) {
|
||||
requestCameraPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cameraOnly && !checkStoragePermission()) {
|
||||
requestStoragePermission();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建选择器
|
||||
showFileChooser();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示文件选择器
|
||||
*/
|
||||
private void showFileChooser() {
|
||||
Intent chooserIntent = createChooserIntent(pendingAcceptImage, pendingAcceptCamera);
|
||||
if (chooserIntent != null) {
|
||||
try {
|
||||
activity.startActivityForResult(chooserIntent, REQUEST_CODE_FILE_CHOOSER);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "打开文件选择器失败", e);
|
||||
Toast.makeText(activity, "打开文件选择器失败", Toast.LENGTH_SHORT).show();
|
||||
if (filePathCallback != null) {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
filePathCallback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建选择器Intent
|
||||
*/
|
||||
private Intent createChooserIntent(boolean acceptImage, boolean acceptCamera) {
|
||||
// 如果只需要相机,直接返回相机Intent
|
||||
if (acceptCamera && pendingCameraOnly) {
|
||||
return createCameraIntent();
|
||||
}
|
||||
|
||||
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
|
||||
Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
|
||||
if (acceptImage) {
|
||||
contentSelectionIntent.setType("image/*");
|
||||
} else {
|
||||
contentSelectionIntent.setType("*/*");
|
||||
}
|
||||
|
||||
Intent[] intentArray;
|
||||
if (acceptCamera) {
|
||||
Intent cameraIntent = createCameraIntent();
|
||||
if (cameraIntent != null) {
|
||||
intentArray = new Intent[]{cameraIntent};
|
||||
} else {
|
||||
intentArray = new Intent[0];
|
||||
}
|
||||
} else {
|
||||
intentArray = new Intent[0];
|
||||
}
|
||||
|
||||
chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
|
||||
chooserIntent.putExtra(Intent.EXTRA_TITLE, "选择文件");
|
||||
|
||||
return chooserIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建相机Intent
|
||||
*/
|
||||
private Intent createCameraIntent() {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
if (cameraIntent.resolveActivity(activity.getPackageManager()) != null) {
|
||||
File photoFile = createImageFile();
|
||||
if (photoFile != null) {
|
||||
cameraPhotoUri = FileProvider.getUriForFile(
|
||||
activity,
|
||||
activity.getPackageName() + ".fileprovider",
|
||||
photoFile
|
||||
);
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraPhotoUri);
|
||||
cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
return cameraIntent;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片文件
|
||||
*/
|
||||
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()) {
|
||||
storageDir.mkdirs();
|
||||
}
|
||||
return File.createTempFile(imageFileName, ".jpg", storageDir);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "创建图片文件失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Activity结果
|
||||
*/
|
||||
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQUEST_CODE_FILE_CHOOSER) {
|
||||
if (filePathCallback == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Uri[] results = null;
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (data != null && data.getData() != null) {
|
||||
// 从文件选择器选择的文件
|
||||
results = new Uri[]{data.getData()};
|
||||
} else if (cameraPhotoUri != null) {
|
||||
// 从相机拍摄的照片
|
||||
results = new Uri[]{cameraPhotoUri};
|
||||
}
|
||||
}
|
||||
|
||||
filePathCallback.onReceiveValue(results);
|
||||
filePathCallback = null;
|
||||
cameraPhotoUri = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理权限请求结果
|
||||
*/
|
||||
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (requestCode == REQUEST_CODE_PERMISSION_CAMERA) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(activity, "相机权限已授予", Toast.LENGTH_SHORT).show();
|
||||
// 权限授予后,检查存储权限
|
||||
if (!checkStoragePermission()) {
|
||||
requestStoragePermission();
|
||||
} else {
|
||||
// 权限都已授予,重新打开文件选择器
|
||||
showFileChooser();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(activity, "相机权限被拒绝", Toast.LENGTH_SHORT).show();
|
||||
if (filePathCallback != null) {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
filePathCallback = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (requestCode == REQUEST_CODE_PERMISSION_STORAGE) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(activity, "存储权限已授予", Toast.LENGTH_SHORT).show();
|
||||
// 权限授予后,重新打开文件选择器
|
||||
showFileChooser();
|
||||
} else {
|
||||
Toast.makeText(activity, "存储权限被拒绝", Toast.LENGTH_SHORT).show();
|
||||
if (filePathCallback != null) {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
filePathCallback = null;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查相机权限
|
||||
*/
|
||||
private boolean checkCameraPermission() {
|
||||
return ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求相机权限
|
||||
*/
|
||||
private void requestCameraPermission() {
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{Manifest.permission.CAMERA},
|
||||
REQUEST_CODE_PERMISSION_CAMERA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查存储权限
|
||||
* Android 10+ 使用分区存储,不需要READ_EXTERNAL_STORAGE权限来访问通过文件选择器选择的文件
|
||||
*/
|
||||
private boolean checkStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= 33) { // Android 13 (TIRAMISU)
|
||||
// Android 13+ 使用新的媒体权限
|
||||
return ContextCompat.checkSelfPermission(activity, "android.permission.READ_MEDIA_IMAGES")
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
} else if (Build.VERSION.SDK_INT >= 29) { // Android 10 (Q)
|
||||
// Android 10-12 使用分区存储,通过文件选择器不需要权限
|
||||
return true;
|
||||
} else {
|
||||
// Android 9 需要存储权限
|
||||
return ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求存储权限
|
||||
*/
|
||||
private void requestStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= 33) { // Android 13 (TIRAMISU)
|
||||
// Android 13+ 请求新的媒体权限
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{"android.permission.READ_MEDIA_IMAGES"},
|
||||
REQUEST_CODE_PERMISSION_STORAGE);
|
||||
} else if (Build.VERSION.SDK_INT >= 29) { // Android 10 (Q)
|
||||
// Android 10-12 不需要请求权限
|
||||
Toast.makeText(activity, "存储权限已授予", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// Android 9 请求存储权限
|
||||
ActivityCompat.requestPermissions(activity,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
REQUEST_CODE_PERMISSION_STORAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名
|
||||
*/
|
||||
public static String getFileName(Context context, Uri uri) {
|
||||
String result = null;
|
||||
if (uri.getScheme().equals("content")) {
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (nameIndex >= 0) {
|
||||
result = cursor.getString(nameIndex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "获取文件名失败", e);
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.getPath();
|
||||
int cut = result.lastIndexOf('/');
|
||||
if (cut != -1) {
|
||||
result = result.substring(cut + 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件转换为Base64字符串
|
||||
*/
|
||||
public static String fileToBase64(Context context, Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
byteArrayOutputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
inputStream.close();
|
||||
|
||||
byte[] fileBytes = byteArrayOutputStream.toByteArray();
|
||||
return Base64.encodeToString(fileBytes, Base64.NO_WRAP);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "文件转Base64失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小
|
||||
*/
|
||||
public static long getFileSize(Context context, Uri uri) {
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
|
||||
if (sizeIndex >= 0) {
|
||||
return cursor.getLong(sizeIndex);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "获取文件大小失败", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
11
app/src/main/res/layout/activity_file_test.xml
Normal file
11
app/src/main/res/layout/activity_file_test.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webViewFileTest"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</RelativeLayout>
|
||||
@ -14,4 +14,10 @@
|
||||
android:orderInCategory="100"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_file_test"
|
||||
android:title="@string/action_file_test"
|
||||
android:orderInCategory="100"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
@ -4,6 +4,8 @@
|
||||
<string name="title_activity_setting_network">网络设置界面</string>
|
||||
<string name="action_setting_voice">语音设置</string>
|
||||
<string name="title_activity_setting_voice">语音设置界面</string>
|
||||
<string name="action_file_test">文件测试</string>
|
||||
<string name="title_activity_file_test">文件选择测试</string>
|
||||
<string name="title_activity_main">主页</string>
|
||||
|
||||
<!-- 讯飞离线语音appid-->
|
||||
|
||||
7
app/src/main/res/xml/file_paths.xml
Normal file
7
app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="external_files" path="." />
|
||||
<external-cache-path name="external_cache" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
<files-path name="files" path="." />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user