最近换了工作,新工作是负责用qml做qt安卓开发。
工作中遇到一个问题:安卓设备有USB口,需要插入一个U盘在程序里读写U盘中的文件,由于安卓系统的安全性的问题导致QFile、c++的文件操作相关方法都不能读写成功,想要读写成功只能调用java代码,在java代码里面使用安卓的DocumentFile库。
经过一番探索,成功解决了问题。qt如何添加java代码不说了,网上有。
下面是具体的java代码:
package com.example.myapplication;import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.util.Log;import androidx.documentfile.provider.DocumentFile;import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;public class DocumentsUtils {private static final String TAG = DocumentsUtils.class.getSimpleName();public static final int OPEN_DOCUMENT_TREE_CODE = 8000;public static String as;private static List sExtSdCardPaths = new ArrayList<>();private DocumentsUtils() {}public static void cleanCache() {sExtSdCardPaths.clear();}/*** Get a list of external SD card paths. (Kitkat or higher.)** @return A list of external SD card paths.*/@TargetApi(Build.VERSION_CODES.KITKAT)private static String[] getExtSdCardPaths(Context context) {if (sExtSdCardPaths.size() > 0) {return sExtSdCardPaths.toArray(new String[0]);}for (File file : context.getExternalFilesDirs("external")) {if (file != null && !file.equals(context.getExternalFilesDir("external"))) {int index = file.getAbsolutePath().lastIndexOf("/Android/data");if (index < 0) {Log.w(TAG, "Unexpected external file dir: " + file.getAbsolutePath());} else {String path = file.getAbsolutePath().substring(0, index);try {path = new File(path).getCanonicalPath();} catch (IOException e) {// Keep non-canonical path.}sExtSdCardPaths.add(path);}}}if (sExtSdCardPaths.isEmpty()) sExtSdCardPaths.add("/storage/sdcard1");return sExtSdCardPaths.toArray(new String[0]);}/*** Determine the main folder of the external SD card containing the given file.** @param file the file.* @return The main folder of the external SD card containing this file, if the file is on an SD* card. Otherwise,* null is returned.*/@TargetApi(Build.VERSION_CODES.KITKAT)private static String getExtSdCardFolder(final File file, Context context) {String[] extSdPaths = getExtSdCardPaths(context);try {for (int i = 0; i < extSdPaths.length; i++) {if (file.getCanonicalPath().startsWith(extSdPaths[i])) {return extSdPaths[i];}}} catch (IOException e) {return null;}return null;}/*** Determine if a file is on external sd card. (Kitkat or higher.)** @param file The file.* @return true if on external sd card.*/@TargetApi(Build.VERSION_CODES.KITKAT)public static boolean isOnExtSdCard(final File file, Context c) {return getExtSdCardFolder(file, c) != null;}/*** Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5).* If the file is not* existing, it is created.** @param file The file.* @param isDirectory flag indicating if the file should be a directory.* @return The DocumentFile*/public static DocumentFile getDocumentFile(final File file, final boolean isDirectory,Context context ) {if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {return DocumentFile.fromFile(file);}String baseFolder = getExtSdCardFolder(file, context);// Log.i(TAG,"lum_ baseFolder " + baseFolder);boolean originalDirectory = false;if (baseFolder == null) {return null;}String relativePath = null;try {String fullPath = file.getCanonicalPath();if (!baseFolder.equals(fullPath)) {relativePath = fullPath.substring(baseFolder.length() + 1);} else {originalDirectory = true;}} catch (IOException e) {return null;} catch (Exception f) {originalDirectory = true;//continue}// String as = PreferenceManager.getDefaultSharedPreferences(context).getString(baseFolder, null);//as = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context).getString(baseFolder, null);String st = as;Uri treeUri = null;if (as != null) treeUri = Uri.parse(as);if (treeUri == null) {return null;}// start with root of SD card and then parse through document tree.DocumentFile document = DocumentFile.fromTreeUri(context, treeUri);if (originalDirectory) return document;String[] parts = relativePath.split("/");for (int i = 0; i < parts.length; i++) {DocumentFile nextDocument = document.findFile(parts[i]);if (nextDocument == null) {if ((i < parts.length - 1) || isDirectory) {nextDocument = document.createDirectory(parts[i]);} else {nextDocument = document.createFile("image", parts[i]);}}document = nextDocument;}return document;}public static boolean mkdirs(Context context, File dir) {boolean res = dir.mkdirs();if (!res) {if (DocumentsUtils.isOnExtSdCard(dir, context)) {DocumentFile documentFile = DocumentsUtils.getDocumentFile(dir, true, context);res = documentFile != null && documentFile.canWrite();}}return res;}public static boolean delete(Context context, File file) {boolean ret = file.delete();if (!ret && DocumentsUtils.isOnExtSdCard(file, context)) {DocumentFile f = DocumentsUtils.getDocumentFile(file, false, context);if (f != null) {ret = f.delete();}}return ret;}public static boolean canWrite(File file) {boolean res = file.exists() && file.canWrite();if (!res && !file.exists()) {try {if (!file.isDirectory()) {res = file.createNewFile() && file.delete();} else {res = file.mkdirs() && file.delete();}} catch (IOException e) {e.printStackTrace();}}return res;}public static boolean canWrite(Context context, File file) {boolean res = canWrite(file);if (!res && DocumentsUtils.isOnExtSdCard(file, context)) {DocumentFile documentFile = DocumentsUtils.getDocumentFile(file, true, context);res = documentFile != null && documentFile.canWrite();}return res;}/*** 重命名* @param context* @param src* @param dest* @return*/public static boolean renameTo(Context context, File src, File dest) {boolean res = src.renameTo(dest);if (!res && isOnExtSdCard(dest, context)) {DocumentFile srcDoc;if (isOnExtSdCard(src, context)) {srcDoc = getDocumentFile(src, false, context);} else {srcDoc = DocumentFile.fromFile(src);}DocumentFile destDoc = getDocumentFile(dest.getParentFile(), true, context);if (srcDoc != null && destDoc != null) {try {Log.i("renameTo", "src.getParent():" + src.getParent() + ",dest.getParent():" + dest.getParent());if (src.getParent().equals(dest.getParent())) {//同一目录res = srcDoc.renameTo(dest.getName());} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//不同一目录Uri renameSrcUri = DocumentsContract.renameDocument(context.getContentResolver(),//先重命名srcDoc.getUri(), dest.getName());res = DocumentsContract.moveDocument(context.getContentResolver(),//再移动renameSrcUri,srcDoc.getParentFile().getUri(),destDoc.getUri()) != null;}} catch (Exception e) {e.printStackTrace();}}}return res;}public static InputStream getInputStream(Context context, File destFile) {InputStream in = null;try {if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) {DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context);if (file != null && file.canWrite()) {in = context.getContentResolver().openInputStream(file.getUri());}} else {in = new FileInputStream(destFile);}} catch (FileNotFoundException e) {e.printStackTrace();}return in;}public static OutputStream getOutputStream(Context context, File destFile) {OutputStream out = null;try {if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) {DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context);if (file != null && file.canWrite()) {out = context.getContentResolver().openOutputStream(file.getUri());}} else {out = new FileOutputStream(destFile);}} catch (FileNotFoundException e) {e.printStackTrace();}return out;}/*** 获取文件流* @param context* @param destFile 目标文件* @param mode May be "w", "wa", "rw", or "rwt".* @return*/public static OutputStream getOutputStream(Context context, File destFile, String mode) {OutputStream out = null;try {if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) {DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context);if (file != null && file.canWrite()) {out = context.getContentResolver().openOutputStream(file.getUri(), mode);}} else {out = new FileOutputStream(destFile, mode.equals("rw") || mode.equals("wa"));}} catch (FileNotFoundException e) {e.printStackTrace();}return out;}public static FileDescriptor getFileDescriptor(Context context, File destFile) {FileDescriptor fd = null;try {if (/*!canWrite(destFile) && */isOnExtSdCard(destFile, context)) {DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context);if (file != null && file.canWrite()) {ParcelFileDescriptor out = context.getContentResolver().openFileDescriptor(file.getUri(), "rw");fd = out.getFileDescriptor();}} else {RandomAccessFile file = null;try {file = new RandomAccessFile(destFile, "rws");file.setLength(0);fd = file.getFD();} catch (Exception e){e.printStackTrace();} finally {if (file != null) {try {file.close();} catch (IOException e) {e.printStackTrace();}}}}} catch (FileNotFoundException e) {e.printStackTrace();}return fd;}public static boolean saveTreeUri(Context context, String rootPath, Uri uri) {DocumentFile file = DocumentFile.fromTreeUri(context, uri);if (file != null && file.canWrite()) {SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context);perf.edit().putString(rootPath, uri.toString()).apply();Log.e(TAG, "save uri" + rootPath);return true;} else {Log.e(TAG, "no write permission: " + rootPath);}return false;}/*** 返回true表示没有权限* @param context* @param rootPath* @return*/public static boolean checkWritableRootPath(Context context, String rootPath) {File root = new File(rootPath);if (!root.canWrite()) {Log.e(TAG,"sd card can not write:" + rootPath + ",is on extsdcard:" + DocumentsUtils.isOnExtSdCard(root, context));if (DocumentsUtils.isOnExtSdCard(root, context)) {DocumentFile documentFile = DocumentsUtils.getDocumentFile(root, true, context);if (documentFile != null) {Log.i(TAG, "get document file:" + documentFile.canWrite());}return documentFile == null || !documentFile.canWrite();} else {SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context);String documentUri = perf.getString(rootPath, "");Log.i(TAG,"lum_2 get perf documentUri:" + documentUri);if (documentUri == null || documentUri.isEmpty()) {return true;} else {DocumentFile file = DocumentFile.fromTreeUri(context, Uri.parse(documentUri));if (file != null)Log.i(TAG,"lum get perf documentUri:" + file.canWrite());return !(file != null && file.canWrite());}}}else{Log.e(TAG,"sd card can write...");}return false;}
}
在网上搜索DocumentFile就会搜出一段处理处理DocumentFile的java代码,都一样的抄来抄去,这段代码也是根据那段代码改的,经过修改可直接放到qt工程里使用。
程序第一次运行时调用(用的qt5):
QAndroidJniObject androidJinObject = QtAndroid::androidActivity();androidJinObject.callMethod("showOpenDocumentTree");
这会弹出申请读写设备权限的弹窗。这个弹窗只弹出一次,再次执行也不会弹出除非卸载应用重新安装。
之后就可以读写了,读写也只能用DocumentFile,QFile依旧不行。
注意这里获取QAndroidJniObject 使用的是QtAndroid::androidActivity(),直接创建一个QAndroidJniObject 对象调用java的static方法可以,非static方法会崩溃,原因未知。
复制文件的例子(不完整):
File dest;if(dest.exists()){dest.delete();}File source;InputStream input = null;OutputStream output = null;try{input = getInputStream(this,source);//通过DocumentFile获取流output = getOutputStream(this,dest);byte[] buf = new byte[1024];int bytesRead;while ((bytesRead = input.read(buf)) > 0){output.write(buf, 0, bytesRead);}}finally{input.close();output.close();}
java导入库后,qt编译时如果提示库文件不存在那么把库文件添加到build.gradle文件中,如:
dependencies {implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])implementation "androidx.documentfile:documentfile:1.0.1"implementation 'androidx.appcompat:appcompat:1.3.0'implementation 'com.google.android.material:material:1.4.0'implementation 'androidx.constraintlayout:constraintlayout:2.0.4'implementation 'androidx.navigation:navigation-fragment:2.3.5'implementation 'androidx.navigation:navigation-ui:2.3.5'implementation 'androidx.preference:preference:1.1.1'
}
此文件编译时会自动生成,若要往build.gradle文件添加内容需要先把这个文件添加到qt工程中,那么再次编译时就用的工程中的此文件。