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:
yao-1212
2026-01-23 17:23:47 +08:00
parent b9897fa0c6
commit 210d599ce0
11 changed files with 876 additions and 1 deletions

View File

@ -11,7 +11,7 @@ android {
minSdk 28 minSdk 28
targetSdk 28 targetSdk 28
versionCode 1 versionCode 1
versionName "2.14" versionName "2.15"
// 1.0 IDATA广播模式处理 // 1.0 IDATA广播模式处理
// 1.1 霍尼韦尔的监听修改扫描网站二维码跳出程序监听失效调整、斑马PDA广播模式设置 // 1.1 霍尼韦尔的监听修改扫描网站二维码跳出程序监听失效调整、斑马PDA广播模式设置
@ -50,6 +50,7 @@ android {
// 2.13 取消监听旋转角度,使用系统自带的旋转(根据配置初始化,旋转方向:横、竖、随意) // 2.13 取消监听旋转角度,使用系统自带的旋转(根据配置初始化,旋转方向:横、竖、随意)
// 瑞芯适配器 接入 新的型号,使用的是 ttyS8而不是ttyS1并且只有一个接口。 // 瑞芯适配器 接入 新的型号,使用的是 ttyS8而不是ttyS1并且只有一个接口。
// 2.14 适配 AIFUU 陈安良:陆军特色中心医院 // 2.14 适配 AIFUU 陈安良:陆军特色中心医院
// 2.15 添加文件选择和相机拍照功能
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk { ndk {
abiFilters 'armeabi-v7a' abiFilters 'armeabi-v7a'

View File

@ -14,6 +14,12 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="com.honeywell.decode.permission.DECODE" /> <uses-permission android:name="com.honeywell.decode.permission.DECODE" />
<uses-permission android:name="android.permission.STOP_SERVICE" /> <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 <application
android:allowBackup="true" android:allowBackup="true"
@ -32,6 +38,9 @@
<activity <activity
android:name=".activity.VoiceSettingActivity" android:name=".activity.VoiceSettingActivity"
android:label="@string/title_activity_setting_voice" /> android:label="@string/title_activity_setting_voice" />
<activity
android:name=".activity.FileTestActivity"
android:label="@string/title_activity_file_test" />
<activity <activity
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
@ -53,6 +62,16 @@
<action android:name="com.example.chaoran.ScanServiceZEBRA"/> <action android:name="com.example.chaoran.ScanServiceZEBRA"/>
</intent-filter> </intent-filter>
</service> </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> </application>
</manifest> </manifest>

View 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
}));
}
-->

View File

@ -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();
}
}
}

View File

@ -16,6 +16,7 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator; import android.os.Vibrator;
@ -58,6 +59,7 @@ import chaoran.business.service.ScanServiceZEBRA;
import chaoran.business.utils.DataCleanManager; import chaoran.business.utils.DataCleanManager;
import chaoran.business.utils.LocalAddressUtil; import chaoran.business.utils.LocalAddressUtil;
import chaoran.business.utils.StatusBarUtil; 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 SettingEngine settingEngine;
private ProgressBar progressBar; private ProgressBar progressBar;
private ActionBar actionBar; private ActionBar actionBar;
private FileChooserHelper fileChooserHelper;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -215,9 +218,11 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
actionBar = getSupportActionBar(); actionBar = getSupportActionBar();
voiceEngine = new TekVoiceEngine(this); voiceEngine = new TekVoiceEngine(this);
settingEngine = new NetworkSettingEngine(this); settingEngine = new NetworkSettingEngine(this);
fileChooserHelper = new FileChooserHelper(this);
webView = findViewById(R.id.webView); webView = findViewById(R.id.webView);
progressBar = findViewById(R.id.loading); progressBar = findViewById(R.id.loading);
webView.setWebViewClient(disposeView()); webView.setWebViewClient(disposeView());
webView.setWebChromeClient(createWebChromeClient());
WebSettings settings = webView.getSettings(); WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true); settings.setJavaScriptEnabled(true);
@ -241,6 +246,48 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
private boolean isPageFinished = false; // 新增标志位 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() { private WebViewClient disposeView() {
return new WebViewClient() { return new WebViewClient() {
@ -391,6 +438,9 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
case R.id.action_setting_voice: case R.id.action_setting_voice:
startActivity(new Intent(this, VoiceSettingActivity.class)); startActivity(new Intent(this, VoiceSettingActivity.class));
break; break;
case R.id.action_file_test:
startActivity(new Intent(this, FileTestActivity.class));
break;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -529,4 +579,20 @@ public class MainActivity extends AppCompatActivity implements ResultListener{
.show(); .show();
// 不调用 super.onBackPressed(),避免直接退出 // 不调用 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)) {
// 如果不是文件选择器的权限请求,可以在这里处理其他权限请求
}
}
} }

View 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;
}
}

View 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;
}
}

View 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>

View File

@ -14,4 +14,10 @@
android:orderInCategory="100" android:orderInCategory="100"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_file_test"
android:title="@string/action_file_test"
android:orderInCategory="100"
app:showAsAction="never" />
</menu> </menu>

View File

@ -4,6 +4,8 @@
<string name="title_activity_setting_network">网络设置界面</string> <string name="title_activity_setting_network">网络设置界面</string>
<string name="action_setting_voice">语音设置</string> <string name="action_setting_voice">语音设置</string>
<string name="title_activity_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> <string name="title_activity_main">主页</string>
<!-- 讯飞离线语音appid--> <!-- 讯飞离线语音appid-->

View 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>