Android获取选中文件路径传入加密方法及onClick报错解决
问题背景
在TextView与Toast中显示文件路径的初始实现代码如下:
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { assert data != null; textView.setText(data.getDataString()); Toast.makeText(this.getApplicationContext(), data.getDataString(), Toast.LENGTH_LONG).show(); super.onActivityResult(requestCode, resultCode, data); }
需求为实现从手机端选择文件后,将文件作为入参传入指定加密方法。此前在Eclipse中使用的加密代码手动传入文件路径时可正常运行,代码如下:
public static void Encrypt(String path) throws Exception{ // 待加密文件 FileInputStream inFile = new FileInputStream(path); // 加密后输出文件 FileOutputStream outFile = new FileOutputStream("encryptedfile.des"); // 加密密码 String password = "password"; byte[] salt = new byte[8]; SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(salt); FileOutputStream saltOutFile = new FileOutputStream("salt.enc"); saltOutFile.write(salt); saltOutFile.close(); SecretKeyFactory factory = SecretKeyFactory .getInstance("PBKDF2WithHmacSHA1"); KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256); SecretKey secretKey = factory.generateSecret(keySpec); SecretKey secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secret); AlgorithmParameters params = cipher.getParameters(); FileOutputStream ivOutFile = new FileOutputStream("iv.enc"); byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); ivOutFile.write(iv); ivOutFile.close(); // 文件加密流程 byte[] input = new byte[64]; int bytesRead; while ((bytesRead = inFile.read(input)) != -1) { byte[] output = cipher.update(input, 0, bytesRead); if (output != null) outFile.write(output); } byte[] output = cipher.doFinal(); if (output != null) outFile.write(output); inFile.close(); outFile.flush(); outFile.close(); }
与加密按钮绑定、调用加密方法的MainActivity初始代码如下:
public class MainActivity extends AppCompatActivity { public static TextView textView,message; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = findViewById(R.id.textView); message = findViewById(R.id.textView2); } public void BrowseButton(View view) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); startActivityForResult(intent, 1); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { assert data != null; textView.setText(data.getDataString()); Toast.makeText(this.getApplicationContext(), data.getDataString(), Toast.LENGTH_LONG).show(); super.onActivityResult(requestCode, resultCode, data); } public void Encrypt(View view) throws Exception { Encryption.Encrypt(); } public void Decrypt(View view) throws Exception{ Decryption.Decrypt(); } }
后续对加密方法的调用逻辑做了修改,修改后代码如下:
public void Encrypt(View view) throws Exception { String path= String.valueOf(textView); Encryption.Encrypt(path); }
当前现象:TextView可正常显示用户浏览选中的文件路径,但运行时报错:java.lang.IllegalStateException: Could not execute method for android:onClick
故障原因
- 取值逻辑错误:
String.valueOf(textView)获取的是TextView控件实例的对象字符串表示,不是控件内展示的文本内容,就算要取文本也应该调用textView.getText().toString() - 核心逻辑错误:
data.getDataString()返回的是Content URI(格式类似content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2Ftest.txt),不是文件在磁盘上的真实绝对路径,直接把该字符串传给FileInputStream会触发文件不存在异常,这是点击按钮崩溃的核心原因 - 健壮性不足:
assert data != null的非空判断在release构建中会被剔除,用户取消选文件时data为null会直接触发空指针崩溃;直接在根目录创建输出文件的写法在Android 10+分区存储模型下会触发权限错误
修复方案
Android 4.4之后系统对存储访问做了多层限制,强行将Content URI转换为真实磁盘路径的方案适配成本极高,且在高版本系统上大概率失效,正确做法是直接通过ContentResolver打开选中文件的输入流,不需要获取真实路径,具体修改步骤如下:
- 在MainActivity中新增成员变量存储选中文件的Uri,修改
onActivityResult逻辑,做好结果校验:
// 成员变量,存储选中文件的Uri private Uri selectedFileUri; @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); // 校验返回结果合法性 if (resultCode == RESULT_OK && requestCode == 1 && data != null) { selectedFileUri = data.getData(); if (selectedFileUri != null) { textView.setText(selectedFileUri.toString()); Toast.makeText(this, "文件选择成功", Toast.LENGTH_SHORT).show(); } } }
- 修改加密方法参数,直接通过ContentResolver获取选中文件的输入流,同时将输出文件存到应用私有目录规避存储权限问题:
// 方法参数改为Context和选中文件的Uri public static void Encrypt(Context context, Uri fileUri) throws Exception{ // 直接通过ContentResolver打开文件输入流,无需真实路径 InputStream inStream = context.getContentResolver().openInputStream(fileUri); if (inStream == null) { throw new FileNotFoundException("选中文件无法打开"); } // 所有输出文件存到应用私有目录,无需额外存储权限 File privateDir = context.getFilesDir(); FileOutputStream outFile = new FileOutputStream(new File(privateDir, "encryptedfile.des")); String password = "password"; byte[] salt = new byte[8]; SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(salt); FileOutputStream saltOutFile = new FileOutputStream(new File(privateDir, "salt.enc")); saltOutFile.write(salt); saltOutFile.close(); SecretKeyFactory factory = SecretKeyFactory .getInstance("PBKDF2WithHmacSHA1"); KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256); SecretKey secretKey = factory.generateSecret(keySpec); SecretKey secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, secret); AlgorithmParameters params = cipher.getParameters(); FileOutputStream ivOutFile = new FileOutputStream(new File(privateDir, "iv.enc")); byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); ivOutFile.write(iv); ivOutFile.close(); // 原有加密逻辑保持不变,仅替换输入流即可 byte[] input = new byte[64]; int bytesRead; while ((bytesRead = inStream.read(input)) != -1) { byte[] output = cipher.update(input, 0, bytesRead); if (output != null) outFile.write(output); } byte[] output = cipher.doFinal(); if (output != null) outFile.write(output); inStream.close(); outFile.flush(); outFile.close(); }
- 修改加密按钮点击逻辑,传入合法参数,增加空校验:
public void Encrypt(View view) throws Exception { if (selectedFileUri == null) { Toast.makeText(this, "请先选择待加密文件", Toast.LENGTH_SHORT).show(); return; } Encryption.Encrypt(this, selectedFileUri); Toast.makeText(this, "加密完成", Toast.LENGTH_SHORT).show(); }
优化提示
- 文件加密属于IO密集型耗时操作,不要直接在主线程执行,否则大文件场景下会触发ANR,实际使用时需要放到子线程执行,可选择协程、WorkManager等方案实现
- 通过
ACTION_GET_CONTENT选择文件不需要申请存储权限,可兼容Android 6.0到Android 14的所有版本 - 如果需要将加密后的文件分享给其他应用,可将文件写入公共目录下的应用专属目录,或通过FileProvider生成Uri分享
内容的提问来源于stack exchange,提问作者OutputNull
相关产品推荐
相关产品推荐

