Android: Add facilities to handle more content URIs operations
Use DocumentFile and DocumentsContract to support more operations on content URIs, such as: * listing files and subdirectories with usable content uris * mkdir, rmdir * creating non-existing files under a tree uri * remove And since dealing with content URIs require some level of user interation, manual tests were added to cover what's been implemented. Note: parts of the code were from from BogDan Vatra <bogdan@kdab.com>. Pick-to: 6.4 6.2 Task-number: QTBUG-98974 Task-number: QTBUG-104776 Change-Id: I3d64958ef26d0155210905b65daae2efa3db31c1 Reviewed-by: Ville Voutilainen <ville.voutilainen@qt.io>
This commit is contained in:
parent
b949f65f60
commit
e5d591a0d0
@ -11,7 +11,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.HashMap;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
@ -83,8 +82,6 @@ public class QtNative
|
||||
private static Boolean m_tabletEventSupported = null;
|
||||
private static boolean m_usePrimaryClip = false;
|
||||
public static QtThread m_qtThread = new QtThread();
|
||||
private static HashMap<String, Uri> m_cachedUris = new HashMap<String, Uri>();
|
||||
private static ArrayList<String> m_knownDirs = new ArrayList<String>();
|
||||
private static final int KEYBOARD_HEIGHT_THRESHOLD = 100;
|
||||
|
||||
private static final String INVALID_OR_NULL_URI_ERROR_MESSAGE = "Received invalid/null Uri";
|
||||
@ -217,209 +214,6 @@ public class QtNative
|
||||
}
|
||||
}
|
||||
|
||||
public static ParcelFileDescriptor openParcelFdForContentUrl(Context context, String contentUrl,
|
||||
String openMode)
|
||||
{
|
||||
Uri uri = m_cachedUris.get(contentUrl);
|
||||
if (uri == null)
|
||||
uri = getUriWithValidPermission(context, contentUrl, openMode);
|
||||
|
||||
if (uri == null) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
return resolver.openFileDescriptor(uri, openMode);
|
||||
} catch (FileNotFoundException | IllegalArgumentException | SecurityException e) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + e.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static FileDescriptor openFdObjectForContentUrl(Context context, String contentUrl,
|
||||
String openMode)
|
||||
{
|
||||
final ParcelFileDescriptor pfd = openParcelFdForContentUrl(context, contentUrl, openMode);
|
||||
if (pfd != null)
|
||||
return pfd.getFileDescriptor();
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int openFdForContentUrl(Context context, String contentUrl, String openMode)
|
||||
{
|
||||
Uri uri = m_cachedUris.get(contentUrl);
|
||||
if (uri == null)
|
||||
uri = getUriWithValidPermission(context, contentUrl, openMode);
|
||||
|
||||
int fileDescriptor = -1;
|
||||
if (uri == null) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE);
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
try {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
fileDescriptor = resolver.openFileDescriptor(uri, openMode).detachFd();
|
||||
} catch (IllegalArgumentException | SecurityException | FileNotFoundException e) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + e.toString());
|
||||
}
|
||||
|
||||
return fileDescriptor;
|
||||
}
|
||||
|
||||
public static long getSize(Context context, String contentUrl)
|
||||
{
|
||||
long size = -1;
|
||||
Uri uri = m_cachedUris.get(contentUrl);
|
||||
if (uri == null)
|
||||
uri = getUriWithValidPermission(context, contentUrl, "r");
|
||||
|
||||
if (uri == null) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE);
|
||||
return size;
|
||||
} else if (!m_cachedUris.containsKey(contentUrl)) {
|
||||
m_cachedUris.put(contentUrl, uri);
|
||||
}
|
||||
|
||||
try {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Cursor cur = resolver.query(uri, new String[] {
|
||||
DocumentsContract.Document.COLUMN_SIZE },
|
||||
null, null, null);
|
||||
if (cur != null) {
|
||||
if (cur.moveToFirst())
|
||||
size = cur.getLong(0);
|
||||
cur.close();
|
||||
}
|
||||
return size;
|
||||
} catch (IllegalArgumentException | SecurityException | UnsupportedOperationException e) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + e.toString());
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public static boolean checkFileExists(Context context, String contentUrl)
|
||||
{
|
||||
boolean exists = false;
|
||||
Uri uri = m_cachedUris.get(contentUrl);
|
||||
if (uri == null)
|
||||
uri = getUriWithValidPermission(context, contentUrl, "r");
|
||||
if (uri == null) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE);
|
||||
return exists;
|
||||
} else {
|
||||
if (!m_cachedUris.containsKey(contentUrl))
|
||||
m_cachedUris.put(contentUrl, uri);
|
||||
}
|
||||
|
||||
try {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Cursor cur = resolver.query(uri, null, null, null, null);
|
||||
if (cur != null) {
|
||||
exists = true;
|
||||
cur.close();
|
||||
}
|
||||
return exists;
|
||||
} catch (IllegalArgumentException | SecurityException | UnsupportedOperationException e) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + e.toString());
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
public static boolean checkIfWritable(Context context, String contentUrl)
|
||||
{
|
||||
return getUriWithValidPermission(context, contentUrl, "w") != null;
|
||||
}
|
||||
|
||||
public static boolean checkIfDir(Context context, String contentUrl)
|
||||
{
|
||||
boolean isDir = false;
|
||||
Uri uri = m_cachedUris.get(contentUrl);
|
||||
if (m_knownDirs.contains(contentUrl))
|
||||
return true;
|
||||
if (uri == null)
|
||||
uri = getUriWithValidPermission(context, contentUrl, "r");
|
||||
|
||||
if (uri == null) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE);
|
||||
return isDir;
|
||||
} else {
|
||||
if (!m_cachedUris.containsKey(contentUrl))
|
||||
m_cachedUris.put(contentUrl, uri);
|
||||
}
|
||||
|
||||
try {
|
||||
final List<String> paths = uri.getPathSegments();
|
||||
// getTreeDocumentId will throw an exception if it is not a directory so check manually
|
||||
if (!paths.get(0).equals("tree"))
|
||||
return false;
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
|
||||
DocumentsContract.getTreeDocumentId(uri));
|
||||
if (!docUri.toString().startsWith(uri.toString()))
|
||||
return false;
|
||||
Cursor cur = resolver.query(docUri, new String[] {
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE },
|
||||
null, null, null);
|
||||
if (cur != null) {
|
||||
if (cur.moveToFirst()) {
|
||||
final String dirStr = new String(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
isDir = cur.getString(0).equals(dirStr);
|
||||
if (isDir)
|
||||
m_knownDirs.add(contentUrl);
|
||||
}
|
||||
cur.close();
|
||||
}
|
||||
return isDir;
|
||||
} catch (IllegalArgumentException | SecurityException | UnsupportedOperationException e) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + e.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String[] listContentsFromTreeUri(Context context, String contentUrl)
|
||||
{
|
||||
Uri treeUri = Uri.parse(contentUrl);
|
||||
final ArrayList<String> results = new ArrayList<>();
|
||||
if (treeUri == null) {
|
||||
Log.e(QtTAG, getCurrentMethodNameLog() + INVALID_OR_NULL_URI_ERROR_MESSAGE);
|
||||
return results.toArray(new String[results.size()]);
|
||||
}
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final Uri docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri,
|
||||
DocumentsContract.getTreeDocumentId(treeUri));
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(docUri,
|
||||
DocumentsContract.getDocumentId(docUri));
|
||||
Cursor c;
|
||||
final String dirStr = DocumentsContract.Document.MIME_TYPE_DIR;
|
||||
try {
|
||||
c = resolver.query(childrenUri, new String[] {
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE },
|
||||
null, null, null);
|
||||
while (c.moveToNext()) {
|
||||
final String fileString = c.getString(1);
|
||||
if (!m_cachedUris.containsKey(contentUrl + "/" + fileString)) {
|
||||
m_cachedUris.put(contentUrl + "/" + fileString,
|
||||
DocumentsContract.buildDocumentUriUsingTree(treeUri,
|
||||
c.getString(0)));
|
||||
}
|
||||
results.add(fileString);
|
||||
if (c.getString(2).equals(dirStr))
|
||||
m_knownDirs.add(contentUrl + "/" + fileString);
|
||||
}
|
||||
c.close();
|
||||
} catch (Exception e) {
|
||||
Log.w(QtTAG, "Failed query: " + e);
|
||||
return results.toArray(new String[results.size()]);
|
||||
}
|
||||
return results.toArray(new String[results.size()]);
|
||||
}
|
||||
|
||||
// this method loads full path libs
|
||||
public static void loadQtLibraries(final ArrayList<String> libraries)
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
|
||||
// Copyright (C) 2021 The Qt Company Ltd.
|
||||
// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
|
||||
// Copyright (C) 2022 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#include "androidcontentfileengine.h"
|
||||
@ -7,16 +7,29 @@
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#include <QtCore/qjnienvironment.h>
|
||||
#include <QtCore/qjniobject.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QtCore/qurl.h>
|
||||
#include <QtCore/qdatetime.h>
|
||||
#include <QtCore/qmimedatabase.h>
|
||||
|
||||
using namespace QNativeInterface;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
AndroidContentFileEngine::AndroidContentFileEngine(const QString &f)
|
||||
: m_file(f)
|
||||
static QJniObject &contentResolverInstance()
|
||||
{
|
||||
setFileName(f);
|
||||
static QJniObject contentResolver;
|
||||
if (!contentResolver.isValid()) {
|
||||
contentResolver = QJniObject(QNativeInterface::QAndroidApplication::context())
|
||||
.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;");
|
||||
}
|
||||
|
||||
return contentResolver;
|
||||
}
|
||||
|
||||
AndroidContentFileEngine::AndroidContentFileEngine(const QString &filename)
|
||||
: m_initialFile(filename),
|
||||
m_documentFile(DocumentFile::parseFromAnyUri(filename))
|
||||
{
|
||||
setFileName(filename);
|
||||
}
|
||||
|
||||
bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
||||
@ -29,6 +42,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
||||
}
|
||||
if (openMode & QFileDevice::WriteOnly) {
|
||||
openModeStr += u'w';
|
||||
if (!m_documentFile->exists()) {
|
||||
if (QUrl(m_initialFile).path().startsWith("/tree/"_L1)) {
|
||||
const int lastSeparatorIndex = m_initialFile.lastIndexOf('/');
|
||||
const QString fileName = m_initialFile.mid(lastSeparatorIndex + 1);
|
||||
|
||||
QString mimeType;
|
||||
const auto mimeTypes = QMimeDatabase().mimeTypesForFileName(fileName);
|
||||
if (!mimeTypes.empty())
|
||||
mimeType = mimeTypes.first().name();
|
||||
else
|
||||
mimeType = "application/octet-stream";
|
||||
|
||||
if (m_documentFile->parent()) {
|
||||
auto createdFile = m_documentFile->parent()->createFile(mimeType, fileName);
|
||||
if (createdFile)
|
||||
m_documentFile = createdFile;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "open(): non-existent content URI with a document type provided";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (openMode & QFileDevice::Truncate) {
|
||||
openModeStr += u't';
|
||||
@ -36,12 +70,10 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
||||
openModeStr += u'a';
|
||||
}
|
||||
|
||||
m_pfd = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative",
|
||||
"openParcelFdForContentUrl",
|
||||
"(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;",
|
||||
QAndroidApplication::context(),
|
||||
QJniObject::fromString(fileName(DefaultName)).object(),
|
||||
QJniObject::fromString(openModeStr).object());
|
||||
m_pfd = contentResolverInstance().callObjectMethod("openFileDescriptor",
|
||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;",
|
||||
m_documentFile->uri().object(),
|
||||
QJniObject::fromString(openModeStr).object());
|
||||
|
||||
if (!m_pfd.isValid())
|
||||
return false;
|
||||
@ -49,8 +81,7 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
||||
const auto fd = m_pfd.callMethod<jint>("getFd", "()I");
|
||||
|
||||
if (fd < 0) {
|
||||
m_pfd.callMethod<void>("close", "()V");
|
||||
m_pfd = QJniObject();
|
||||
closeNativeFileDescriptor();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -58,48 +89,122 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
||||
}
|
||||
|
||||
bool AndroidContentFileEngine::close()
|
||||
{
|
||||
closeNativeFileDescriptor();
|
||||
return QFSFileEngine::close();
|
||||
}
|
||||
|
||||
void AndroidContentFileEngine::closeNativeFileDescriptor()
|
||||
{
|
||||
if (m_pfd.isValid()) {
|
||||
m_pfd.callMethod<void>("close", "()V");
|
||||
m_pfd = QJniObject();
|
||||
}
|
||||
|
||||
return QFSFileEngine::close();
|
||||
}
|
||||
|
||||
qint64 AndroidContentFileEngine::size() const
|
||||
{
|
||||
const jlong size = QJniObject::callStaticMethod<jlong>(
|
||||
"org/qtproject/qt/android/QtNative", "getSize",
|
||||
"(Landroid/content/Context;Ljava/lang/String;)J", QAndroidApplication::context(),
|
||||
QJniObject::fromString(fileName(DefaultName)).object());
|
||||
return (qint64)size;
|
||||
return m_documentFile->length();
|
||||
}
|
||||
|
||||
bool AndroidContentFileEngine::remove()
|
||||
{
|
||||
return m_documentFile->remove();
|
||||
}
|
||||
|
||||
bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories,
|
||||
std::optional<QFileDevice::Permissions> permissions) const
|
||||
{
|
||||
Q_UNUSED(permissions)
|
||||
|
||||
QString tmp = dirName;
|
||||
tmp.remove(m_initialFile);
|
||||
|
||||
QStringList dirParts = tmp.split(u'/');
|
||||
dirParts.removeAll("");
|
||||
|
||||
if (dirParts.isEmpty())
|
||||
return false;
|
||||
|
||||
auto createdDir = m_documentFile;
|
||||
bool allDirsCreated = true;
|
||||
for (const auto &dir : dirParts) {
|
||||
// Find if the sub-dir already exists and then don't re-create it
|
||||
bool subDirExists = false;
|
||||
for (const DocumentFilePtr &subDir : m_documentFile->listFiles()) {
|
||||
if (dir == subDir->name() && subDir->isDirectory()) {
|
||||
createdDir = subDir;
|
||||
subDirExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!subDirExists) {
|
||||
createdDir = createdDir->createDirectory(dir);
|
||||
if (!createdDir) {
|
||||
allDirsCreated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!createParentDirectories)
|
||||
break;
|
||||
}
|
||||
|
||||
return allDirsCreated;
|
||||
}
|
||||
|
||||
bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const
|
||||
{
|
||||
if (recurseParentDirectories)
|
||||
qWarning() << "rmpath(): Unsupported for Content URIs";
|
||||
|
||||
const QString dirFileName = QUrl(dirName).fileName();
|
||||
bool deleted = false;
|
||||
for (const DocumentFilePtr &dir : m_documentFile->listFiles()) {
|
||||
if (dirFileName == dir->name() && dir->isDirectory()) {
|
||||
deleted = dir->remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
QByteArray AndroidContentFileEngine::id() const
|
||||
{
|
||||
return m_documentFile->id().toUtf8();
|
||||
}
|
||||
|
||||
QDateTime AndroidContentFileEngine::fileTime(FileTime time) const
|
||||
{
|
||||
switch (time) {
|
||||
case FileTime::ModificationTime:
|
||||
return m_documentFile->lastModified();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QDateTime();
|
||||
}
|
||||
|
||||
AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlags type) const
|
||||
{
|
||||
FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag);
|
||||
FileFlags flags;
|
||||
const bool isDir = QJniObject::callStaticMethod<jboolean>(
|
||||
"org/qtproject/qt/android/QtNative", "checkIfDir",
|
||||
"(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
|
||||
QJniObject::fromString(fileName(DefaultName)).object());
|
||||
// If it is a directory then we know it exists so there is no reason to explicitly check
|
||||
const bool exists = isDir ? true : QJniObject::callStaticMethod<jboolean>(
|
||||
"org/qtproject/qt/android/QtNative", "checkFileExists",
|
||||
"(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
|
||||
QJniObject::fromString(fileName(DefaultName)).object());
|
||||
if (!exists && !isDir)
|
||||
if (!m_documentFile->exists())
|
||||
return flags;
|
||||
if (isDir) {
|
||||
flags = DirectoryType | commonFlags;
|
||||
|
||||
flags = ExistsFlag;
|
||||
if (!m_documentFile->canRead())
|
||||
return flags;
|
||||
|
||||
flags |= ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm;
|
||||
|
||||
if (m_documentFile->isDirectory()) {
|
||||
flags |= DirectoryType;
|
||||
} else {
|
||||
flags = FileType | commonFlags;
|
||||
const bool writable = QJniObject::callStaticMethod<jboolean>(
|
||||
"org/qtproject/qt/android/QtNative", "checkIfWritable",
|
||||
"(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
|
||||
QJniObject::fromString(fileName(DefaultName)).object());
|
||||
if (writable)
|
||||
flags |= FileType;
|
||||
if (m_documentFile->canWrite())
|
||||
flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm;
|
||||
}
|
||||
return type & flags;
|
||||
@ -114,18 +219,18 @@ QString AndroidContentFileEngine::fileName(FileName f) const
|
||||
case DefaultName:
|
||||
case AbsoluteName:
|
||||
case CanonicalName:
|
||||
return m_file;
|
||||
return m_documentFile->uri().toString();
|
||||
case BaseName:
|
||||
{
|
||||
const qsizetype pos = m_file.lastIndexOf(u'/');
|
||||
return m_file.mid(pos);
|
||||
}
|
||||
return m_documentFile->name();
|
||||
default:
|
||||
return QString();
|
||||
break;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, const QStringList &filterNames)
|
||||
QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters,
|
||||
const QStringList &filterNames)
|
||||
{
|
||||
return new AndroidContentFileEngineIterator(filters, filterNames);
|
||||
}
|
||||
@ -166,42 +271,468 @@ QString AndroidContentFileEngineIterator::next()
|
||||
|
||||
bool AndroidContentFileEngineIterator::hasNext() const
|
||||
{
|
||||
if (m_index == -1) {
|
||||
if (path().isEmpty())
|
||||
if (m_index == -1 && m_files.isEmpty()) {
|
||||
const auto currentPath = path();
|
||||
if (currentPath.isEmpty())
|
||||
return false;
|
||||
const bool isDir = QJniObject::callStaticMethod<jboolean>(
|
||||
"org/qtproject/qt/android/QtNative", "checkIfDir",
|
||||
"(Landroid/content/Context;Ljava/lang/String;)Z",
|
||||
QAndroidApplication::context(),
|
||||
QJniObject::fromString(path()).object());
|
||||
if (isDir) {
|
||||
QJniObject objArray = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative",
|
||||
"listContentsFromTreeUri",
|
||||
"(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;",
|
||||
QAndroidApplication::context(),
|
||||
QJniObject::fromString(path()).object());
|
||||
if (objArray.isValid()) {
|
||||
QJniEnvironment env;
|
||||
const jsize length = env->GetArrayLength(objArray.object<jarray>());
|
||||
for (int i = 0; i != length; ++i) {
|
||||
m_entries << QJniObject(env->GetObjectArrayElement(
|
||||
objArray.object<jobjectArray>(), i)).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
m_index = 0;
|
||||
|
||||
const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath);
|
||||
if (iterDoc->isDirectory())
|
||||
for (const auto &doc : iterDoc->listFiles())
|
||||
m_files.append(doc);
|
||||
}
|
||||
return m_index < m_entries.size();
|
||||
|
||||
return m_index < (m_files.size() - 1);
|
||||
}
|
||||
|
||||
QString AndroidContentFileEngineIterator::currentFileName() const
|
||||
{
|
||||
if (m_index <= 0 || m_index > m_entries.size())
|
||||
if (m_index < 0 || m_index > m_files.size())
|
||||
return QString();
|
||||
return m_entries.at(m_index - 1);
|
||||
// Returns a full path since contstructing a content path from the file name
|
||||
// and a tree URI only will not point to a valid file URI.
|
||||
return m_files.at(m_index)->uri().toString();
|
||||
}
|
||||
|
||||
QString AndroidContentFileEngineIterator::currentFilePath() const
|
||||
{
|
||||
return currentFileName();
|
||||
}
|
||||
|
||||
// Start of Cursor
|
||||
|
||||
class Cursor
|
||||
{
|
||||
public:
|
||||
explicit Cursor(const QJniObject &object)
|
||||
: m_object{object} { }
|
||||
|
||||
~Cursor()
|
||||
{
|
||||
if (m_object.isValid())
|
||||
m_object.callMethod<void>("close");
|
||||
}
|
||||
|
||||
enum Type {
|
||||
FIELD_TYPE_NULL = 0x00000000,
|
||||
FIELD_TYPE_INTEGER = 0x00000001,
|
||||
FIELD_TYPE_FLOAT = 0x00000002,
|
||||
FIELD_TYPE_STRING = 0x00000003,
|
||||
FIELD_TYPE_BLOB = 0x00000004
|
||||
};
|
||||
|
||||
QVariant data(int columnIndex) const
|
||||
{
|
||||
int type = m_object.callMethod<jint>("getType", "(I)I", columnIndex);
|
||||
switch (type) {
|
||||
case FIELD_TYPE_NULL:
|
||||
return {};
|
||||
case FIELD_TYPE_INTEGER:
|
||||
return QVariant::fromValue(m_object.callMethod<jlong>("getLong", "(I)J", columnIndex));
|
||||
case FIELD_TYPE_FLOAT:
|
||||
return QVariant::fromValue(m_object.callMethod<jdouble>("getDouble", "(I)D",
|
||||
columnIndex));
|
||||
case FIELD_TYPE_STRING:
|
||||
return QVariant::fromValue(m_object.callObjectMethod("getString",
|
||||
"(I)Ljava/lang/String;",
|
||||
columnIndex).toString());
|
||||
case FIELD_TYPE_BLOB: {
|
||||
auto blob = m_object.callObjectMethod("getBlob", "(I)[B", columnIndex);
|
||||
QJniEnvironment env;
|
||||
const auto blobArray = blob.object<jbyteArray>();
|
||||
const int size = env->GetArrayLength(blobArray);
|
||||
const auto byteArray = env->GetByteArrayElements(blobArray, nullptr);
|
||||
QByteArray data{reinterpret_cast<const char *>(byteArray), size};
|
||||
env->ReleaseByteArrayElements(blobArray, byteArray, 0);
|
||||
return QVariant::fromValue(data);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::unique_ptr<Cursor> queryUri(const QJniObject &uri,
|
||||
const QStringList &projection = {},
|
||||
const QString &selection = {},
|
||||
const QStringList &selectionArgs = {},
|
||||
const QString &sortOrder = {})
|
||||
{
|
||||
auto cursor = contentResolverInstance().callObjectMethod("query",
|
||||
"(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;",
|
||||
uri.object(),
|
||||
projection.isEmpty() ? nullptr : fromStringList(projection).object(),
|
||||
selection.isEmpty() ? nullptr : QJniObject::fromString(selection).object(),
|
||||
selectionArgs.isEmpty() ? nullptr : fromStringList(selectionArgs).object(),
|
||||
sortOrder.isEmpty() ? nullptr : QJniObject::fromString(sortOrder).object());
|
||||
if (!cursor.isValid())
|
||||
return {};
|
||||
return std::make_unique<Cursor>(cursor);
|
||||
}
|
||||
|
||||
static QVariant queryColumn(const QJniObject &uri, const QString &column)
|
||||
{
|
||||
const auto query = queryUri(uri, {column});
|
||||
if (!query)
|
||||
return {};
|
||||
|
||||
if (query->rowCount() != 1 || query->columnCount() != 1)
|
||||
return {};
|
||||
query->moveToFirst();
|
||||
return query->data(0);
|
||||
}
|
||||
|
||||
bool isNull(int columnIndex) const
|
||||
{
|
||||
return m_object.callMethod<jboolean>("isNull", "(I)Z", columnIndex);
|
||||
}
|
||||
|
||||
int columnCount() const { return m_object.callMethod<jint>("getColumnCount"); }
|
||||
int rowCount() const { return m_object.callMethod<jint>("getCount"); }
|
||||
int row() const { return m_object.callMethod<jint>("getPosition"); }
|
||||
bool isFirst() const { return m_object.callMethod<jboolean>("isFirst"); }
|
||||
bool isLast() const { return m_object.callMethod<jboolean>("isLast"); }
|
||||
bool moveToFirst() { return m_object.callMethod<jboolean>("moveToFirst"); }
|
||||
bool moveToLast() { return m_object.callMethod<jboolean>("moveToLast"); }
|
||||
bool moveToNext() { return m_object.callMethod<jboolean>("moveToNext"); }
|
||||
|
||||
private:
|
||||
static QJniObject fromStringList(const QStringList &list)
|
||||
{
|
||||
QJniEnvironment env;
|
||||
auto array = env->NewObjectArray(list.size(), env->FindClass("java/lang/String"), nullptr);
|
||||
for (int i = 0; i < list.size(); ++i)
|
||||
env->SetObjectArrayElement(array, i, QJniObject::fromString(list[i]).object());
|
||||
return QJniObject::fromLocalRef(array);
|
||||
}
|
||||
|
||||
QJniObject m_object;
|
||||
};
|
||||
|
||||
// End of Cursor
|
||||
|
||||
// Start of DocumentsContract
|
||||
|
||||
/*!
|
||||
*
|
||||
* DocumentsContract Api.
|
||||
* Check https://developer.android.com/reference/android/provider/DocumentsContract
|
||||
* for more information.
|
||||
*
|
||||
* \note This does not implement all facilities of the native API.
|
||||
*
|
||||
*/
|
||||
namespace DocumentsContract
|
||||
{
|
||||
|
||||
namespace Document {
|
||||
const QLatin1String COLUMN_DISPLAY_NAME("_display_name");
|
||||
const QLatin1String COLUMN_DOCUMENT_ID("document_id");
|
||||
const QLatin1String COLUMN_FLAGS("flags");
|
||||
const QLatin1String COLUMN_LAST_MODIFIED("last_modified");
|
||||
const QLatin1String COLUMN_MIME_TYPE("mime_type");
|
||||
const QLatin1String COLUMN_SIZE("_size");
|
||||
|
||||
constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008;
|
||||
constexpr int FLAG_SUPPORTS_DELETE = 0x00000004;
|
||||
constexpr int FLAG_SUPPORTS_WRITE = 0x00000002;
|
||||
constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200;
|
||||
|
||||
const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory");
|
||||
} // namespace Document
|
||||
|
||||
QString documentId(const QJniObject &uri)
|
||||
{
|
||||
return QJniObject::callStaticObjectMethod("android/provider/DocumentsContract",
|
||||
"getDocumentId",
|
||||
"(Landroid/net/Uri;)Ljava/lang/String;",
|
||||
uri.object()).toString();
|
||||
}
|
||||
|
||||
QString treeDocumentId(const QJniObject &uri)
|
||||
{
|
||||
return QJniObject::callStaticObjectMethod("android/provider/DocumentsContract",
|
||||
"getTreeDocumentId",
|
||||
"(Landroid/net/Uri;)Ljava/lang/String;",
|
||||
uri.object()).toString();
|
||||
}
|
||||
|
||||
QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId)
|
||||
{
|
||||
return QJniObject::callStaticObjectMethod("android/provider/DocumentsContract",
|
||||
"buildChildDocumentsUriUsingTree",
|
||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
||||
uri.object(),
|
||||
QJniObject::fromString(parentDocumentId).object());
|
||||
|
||||
}
|
||||
|
||||
QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId)
|
||||
{
|
||||
return QJniObject::callStaticObjectMethod("android/provider/DocumentsContract",
|
||||
"buildDocumentUriUsingTree",
|
||||
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;",
|
||||
treeUri.object(),
|
||||
QJniObject::fromString(documentId).object());
|
||||
}
|
||||
|
||||
bool isDocumentUri(const QJniObject &uri)
|
||||
{
|
||||
return QJniObject::callStaticMethod<jboolean>("android/provider/DocumentsContract",
|
||||
"isDocumentUri",
|
||||
"(Landroid/content/Context;Landroid/net/Uri;)Z",
|
||||
QNativeInterface::QAndroidApplication::context(),
|
||||
uri.object());
|
||||
}
|
||||
|
||||
bool isTreeUri(const QJniObject &uri)
|
||||
{
|
||||
return QJniObject::callStaticMethod<jboolean>("android/provider/DocumentsContract",
|
||||
"isTreeUri",
|
||||
"(Landroid/net/Uri;)Z",
|
||||
uri.object());
|
||||
}
|
||||
|
||||
QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType,
|
||||
const QString &displayName)
|
||||
{
|
||||
return QJniObject::callStaticObjectMethod("android/provider/DocumentsContract",
|
||||
"createDocument",
|
||||
"(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;",
|
||||
contentResolverInstance().object(),
|
||||
parentDocumentUri.object(),
|
||||
QJniObject::fromString(mimeType).object(),
|
||||
QJniObject::fromString(displayName).object());
|
||||
}
|
||||
|
||||
bool deleteDocument(const QJniObject &documentUri)
|
||||
{
|
||||
const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
|
||||
if (!(flags & Document::FLAG_SUPPORTS_DELETE))
|
||||
return {};
|
||||
|
||||
return QJniObject::callStaticMethod<jboolean>("android/provider/DocumentsContract",
|
||||
"deleteDocument",
|
||||
"(Landroid/content/ContentResolver;Landroid/net/Uri;)Z",
|
||||
contentResolverInstance().object(),
|
||||
documentUri.object());
|
||||
}
|
||||
|
||||
} // End DocumentsContract namespace
|
||||
|
||||
// Start of DocumentFile
|
||||
|
||||
using namespace DocumentsContract;
|
||||
|
||||
namespace {
|
||||
class MakeableDocumentFile : public DocumentFile
|
||||
{
|
||||
public:
|
||||
MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {})
|
||||
: DocumentFile(uri, parent)
|
||||
{}
|
||||
};
|
||||
}
|
||||
|
||||
DocumentFile::DocumentFile(const QJniObject &uri,
|
||||
const DocumentFilePtr &parent)
|
||||
: m_uri{uri}
|
||||
, m_parent{parent}
|
||||
{}
|
||||
|
||||
QJniObject parseUri(const QString &uri)
|
||||
{
|
||||
return QJniObject::callStaticObjectMethod("android/net/Uri",
|
||||
"parse",
|
||||
"(Ljava/lang/String;)Landroid/net/Uri;",
|
||||
QJniObject::fromString(uri).object());
|
||||
}
|
||||
|
||||
DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName)
|
||||
{
|
||||
const QJniObject uri = parseUri(fileName);
|
||||
|
||||
if (DocumentsContract::isDocumentUri(uri))
|
||||
return fromSingleUri(uri);
|
||||
|
||||
const QString documentType = "/document/"_L1;
|
||||
const QString treeType = "/tree/"_L1;
|
||||
|
||||
const int treeIndex = fileName.indexOf(treeType);
|
||||
const int documentIndex = fileName.indexOf(documentType);
|
||||
const int index = fileName.lastIndexOf("/");
|
||||
|
||||
if (index <= treeIndex + treeType.size() || index <= documentIndex + documentType.size())
|
||||
return fromTreeUri(uri);
|
||||
|
||||
const QString parentUrl = fileName.left(index);
|
||||
DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl));
|
||||
|
||||
const QString baseName = fileName.mid(index);
|
||||
const QString fileUrl = parentUrl + QUrl::toPercentEncoding(baseName);
|
||||
|
||||
DocumentFilePtr docFile = std::make_shared<MakeableDocumentFile>(parseUri(fileUrl));
|
||||
if (parentDocFile && parentDocFile->isDirectory())
|
||||
docFile->m_parent = parentDocFile;
|
||||
|
||||
return docFile;
|
||||
}
|
||||
|
||||
DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri)
|
||||
{
|
||||
return std::make_shared<MakeableDocumentFile>(uri);
|
||||
}
|
||||
|
||||
DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri)
|
||||
{
|
||||
QString docId;
|
||||
if (isDocumentUri(treeUri))
|
||||
docId = documentId(treeUri);
|
||||
else
|
||||
docId = treeDocumentId(treeUri);
|
||||
|
||||
return std::make_shared<MakeableDocumentFile>(buildDocumentUriUsingTree(treeUri, docId));
|
||||
}
|
||||
|
||||
DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName)
|
||||
{
|
||||
if (isDirectory()) {
|
||||
return std::make_shared<MakeableDocumentFile>(
|
||||
createDocument(m_uri, mimeType, displayName),
|
||||
shared_from_this());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
DocumentFilePtr DocumentFile::createDirectory(const QString &displayName)
|
||||
{
|
||||
if (isDirectory()) {
|
||||
return std::make_shared<MakeableDocumentFile>(
|
||||
createDocument(m_uri, Document::MIME_TYPE_DIR, displayName),
|
||||
shared_from_this());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const QJniObject &DocumentFile::uri() const
|
||||
{
|
||||
return m_uri;
|
||||
}
|
||||
|
||||
const DocumentFilePtr &DocumentFile::parent() const
|
||||
{
|
||||
return m_parent;
|
||||
}
|
||||
|
||||
QString DocumentFile::name() const
|
||||
{
|
||||
return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString();
|
||||
}
|
||||
|
||||
QString DocumentFile::id() const
|
||||
{
|
||||
return DocumentsContract::documentId(uri());
|
||||
}
|
||||
|
||||
QString DocumentFile::mimeType() const
|
||||
{
|
||||
return Cursor::queryColumn(m_uri, Document::COLUMN_MIME_TYPE).toString();
|
||||
}
|
||||
|
||||
bool DocumentFile::isDirectory() const
|
||||
{
|
||||
return mimeType() == Document::MIME_TYPE_DIR;
|
||||
}
|
||||
|
||||
bool DocumentFile::isFile() const
|
||||
{
|
||||
const QString type = mimeType();
|
||||
return type != Document::MIME_TYPE_DIR && !type.isEmpty();
|
||||
}
|
||||
|
||||
bool DocumentFile::isVirtual() const
|
||||
{
|
||||
return isDocumentUri(m_uri) && (Cursor::queryColumn(m_uri,
|
||||
Document::COLUMN_FLAGS).toInt() & Document::FLAG_VIRTUAL_DOCUMENT);
|
||||
}
|
||||
|
||||
QDateTime DocumentFile::lastModified() const
|
||||
{
|
||||
const auto timeVariant = Cursor::queryColumn(m_uri, Document::COLUMN_LAST_MODIFIED);
|
||||
if (timeVariant.isValid())
|
||||
return QDateTime::fromMSecsSinceEpoch(timeVariant.toLongLong());
|
||||
return {};
|
||||
}
|
||||
|
||||
int64_t DocumentFile::length() const
|
||||
{
|
||||
return Cursor::queryColumn(m_uri, Document::COLUMN_SIZE).toLongLong();
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
|
||||
constexpr int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
|
||||
}
|
||||
|
||||
bool DocumentFile::canRead() const
|
||||
{
|
||||
const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
|
||||
const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
|
||||
"(Landroid/net/Uri;I)I",
|
||||
m_uri.object(),
|
||||
FLAG_GRANT_READ_URI_PERMISSION);
|
||||
if (selfUriPermission != 0)
|
||||
return false;
|
||||
|
||||
return !mimeType().isEmpty();
|
||||
}
|
||||
|
||||
bool DocumentFile::canWrite() const
|
||||
{
|
||||
const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
|
||||
const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
|
||||
"(Landroid/net/Uri;I)I",
|
||||
m_uri.object(),
|
||||
FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (selfUriPermission != 0)
|
||||
return false;
|
||||
|
||||
const QString type = mimeType();
|
||||
if (type.isEmpty())
|
||||
return false;
|
||||
|
||||
const int flags = Cursor::queryColumn(m_uri, Document::COLUMN_FLAGS).toInt();
|
||||
if (flags & Document::FLAG_SUPPORTS_DELETE)
|
||||
return true;
|
||||
|
||||
const bool supportsWrite = (flags & Document::FLAG_SUPPORTS_WRITE);
|
||||
const bool isDir = (type == Document::MIME_TYPE_DIR);
|
||||
const bool dirSupportsCreate = (isDir && (flags & Document::FLAG_DIR_SUPPORTS_CREATE));
|
||||
|
||||
return dirSupportsCreate || supportsWrite;
|
||||
}
|
||||
|
||||
bool DocumentFile::remove()
|
||||
{
|
||||
return deleteDocument(m_uri);
|
||||
}
|
||||
|
||||
bool DocumentFile::exists() const
|
||||
{
|
||||
return !name().isEmpty();
|
||||
}
|
||||
|
||||
std::vector<DocumentFilePtr> DocumentFile::listFiles()
|
||||
{
|
||||
std::vector<DocumentFilePtr> res;
|
||||
const auto childrenUri = buildChildDocumentsUriUsingTree(m_uri, documentId(m_uri));
|
||||
const auto query = Cursor::queryUri(childrenUri, {Document::COLUMN_DOCUMENT_ID});
|
||||
if (!query)
|
||||
return res;
|
||||
|
||||
while (query->moveToNext()) {
|
||||
const auto uri = buildDocumentUriUsingTree(m_uri, query->data(0).toString());
|
||||
res.push_back(std::make_shared<MakeableDocumentFile>(uri, shared_from_this()));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// End of DocumentFile
|
||||
|
@ -5,7 +5,11 @@
|
||||
#define ANDROIDCONTENTFILEENGINE_H
|
||||
|
||||
#include <private/qfsfileengine_p.h>
|
||||
|
||||
#include <QtCore/qjniobject.h>
|
||||
#include <QtCore/qlist.h>
|
||||
|
||||
using DocumentFilePtr = std::shared_ptr<class DocumentFile>;
|
||||
|
||||
class AndroidContentFileEngine : public QFSFileEngine
|
||||
{
|
||||
@ -14,14 +18,24 @@ public:
|
||||
bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override;
|
||||
bool close() override;
|
||||
qint64 size() const override;
|
||||
bool remove() override;
|
||||
bool mkdir(const QString &dirName, bool createParentDirectories,
|
||||
std::optional<QFile::Permissions> permissions = std::nullopt) const override;
|
||||
bool rmdir(const QString &dirName, bool recurseParentDirectories) const override;
|
||||
QByteArray id() const override;
|
||||
bool caseSensitive() const override { return true; }
|
||||
QDateTime fileTime(FileTime time) const override;
|
||||
FileFlags fileFlags(FileFlags type = FileInfoAll) const override;
|
||||
QString fileName(FileName file = DefaultName) const override;
|
||||
QAbstractFileEngine::Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override;
|
||||
QAbstractFileEngine::Iterator *endEntryList() override;
|
||||
private:
|
||||
QString m_file;
|
||||
QJniObject m_pfd;
|
||||
|
||||
private:
|
||||
void closeNativeFileDescriptor();
|
||||
|
||||
QString m_initialFile;
|
||||
QJniObject m_pfd;
|
||||
DocumentFilePtr m_documentFile;
|
||||
};
|
||||
|
||||
class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler
|
||||
@ -42,8 +56,48 @@ public:
|
||||
QString currentFileName() const override;
|
||||
QString currentFilePath() const override;
|
||||
private:
|
||||
mutable QStringList m_entries;
|
||||
mutable int m_index = -1;
|
||||
mutable QList<DocumentFilePtr> m_files;
|
||||
mutable qsizetype m_index = -1;
|
||||
};
|
||||
|
||||
/*!
|
||||
*
|
||||
* DocumentFile Api.
|
||||
* Check https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile
|
||||
* for more information.
|
||||
*
|
||||
*/
|
||||
class DocumentFile : public std::enable_shared_from_this<DocumentFile>
|
||||
{
|
||||
public:
|
||||
static DocumentFilePtr parseFromAnyUri(const QString &filename);
|
||||
static DocumentFilePtr fromSingleUri(const QJniObject &uri);
|
||||
static DocumentFilePtr fromTreeUri(const QJniObject &treeUri);
|
||||
|
||||
DocumentFilePtr createFile(const QString &mimeType, const QString &displayName);
|
||||
DocumentFilePtr createDirectory(const QString &displayName);
|
||||
const QJniObject &uri() const;
|
||||
const DocumentFilePtr &parent() const;
|
||||
QString name() const;
|
||||
QString id() const;
|
||||
QString mimeType() const;
|
||||
bool isDirectory() const;
|
||||
bool isFile() const;
|
||||
bool isVirtual() const;
|
||||
QDateTime lastModified() const;
|
||||
int64_t length() const;
|
||||
bool canRead() const;
|
||||
bool canWrite() const;
|
||||
bool remove();
|
||||
bool exists() const;
|
||||
std::vector<DocumentFilePtr> listFiles();
|
||||
|
||||
protected:
|
||||
DocumentFile(const QJniObject &uri, const std::shared_ptr<DocumentFile> &parent);
|
||||
|
||||
protected:
|
||||
QJniObject m_uri;
|
||||
DocumentFilePtr m_parent;
|
||||
};
|
||||
|
||||
#endif // ANDROIDCONTENTFILEENGINE_H
|
||||
|
@ -89,3 +89,7 @@ endif()
|
||||
if(QT_FEATURE_vulkan)
|
||||
add_subdirectory(qvulkaninstance)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
add_subdirectory(android_content_uri)
|
||||
endif()
|
||||
|
7
tests/manual/android_content_uri/CMakeLists.txt
Normal file
7
tests/manual/android_content_uri/CMakeLists.txt
Normal file
@ -0,0 +1,7 @@
|
||||
qt_internal_add_test(tst_content_uris
|
||||
SOURCES
|
||||
tst_content_uris.cpp
|
||||
LIBRARIES
|
||||
Qt::CorePrivate
|
||||
Qt::Widgets
|
||||
)
|
203
tests/manual/android_content_uri/tst_content_uris.cpp
Normal file
203
tests/manual/android_content_uri/tst_content_uris.cpp
Normal file
@ -0,0 +1,203 @@
|
||||
// Copyright (C) 2022 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include <QTest>
|
||||
#include <QDirIterator>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
class tst_ContentUris: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void dirFacilities();
|
||||
void readWriteFile();
|
||||
void readWriteNonExistingFile_data();
|
||||
void readWriteNonExistingFile();
|
||||
void createFileFromDirUrl_data();
|
||||
void createFileFromDirUrl();
|
||||
void fileOperations();
|
||||
};
|
||||
|
||||
static QStringList listFiles(const QDir &dir, QDirIterator::IteratorFlag flag = {})
|
||||
{
|
||||
QDirIterator it(dir, flag);
|
||||
QStringList dirs;
|
||||
while (it.hasNext())
|
||||
dirs << it.next();
|
||||
return dirs;
|
||||
}
|
||||
|
||||
void showInstructionsDialog(const QString &message)
|
||||
{
|
||||
QMessageBox::information(nullptr, "Instructions", message);
|
||||
}
|
||||
|
||||
void tst_ContentUris::dirFacilities()
|
||||
{
|
||||
showInstructionsDialog("Choose a folder with no content/files/subdirs");
|
||||
|
||||
auto url = QFileDialog::getExistingDirectory();
|
||||
QVERIFY(url.startsWith("content"_L1));
|
||||
QDir dir(url);
|
||||
|
||||
QVERIFY(dir.exists());
|
||||
QVERIFY(!dir.dirName().isEmpty());
|
||||
QVERIFY(listFiles(dir).isEmpty());
|
||||
|
||||
QVERIFY(dir.mkdir("Sub"));
|
||||
const auto dirList = listFiles(dir);
|
||||
QVERIFY(dirList.size() == 1);
|
||||
const QDir subDir = dirList.first();
|
||||
|
||||
QVERIFY(subDir.dirName() == "Sub"_L1);
|
||||
QEXPECT_FAIL("", "absolutePath() is returning wrong path, cutting from 'primary' onward", Continue);
|
||||
qWarning() << "subDir.absolutePath()" << subDir.absolutePath() << dirList.first();
|
||||
QVERIFY(subDir.absolutePath() == dirList.first());
|
||||
QVERIFY(subDir.path() == dirList.first());
|
||||
|
||||
QVERIFY(listFiles(dir, QDirIterator::Subdirectories).size() == 1);
|
||||
QVERIFY(dir.mkdir("Sub")); // Create an existing dir
|
||||
QVERIFY(dir.rmdir("Sub"));
|
||||
|
||||
QVERIFY(dir.mkpath("Sub/Sub2/Sub3"));
|
||||
QVERIFY(listFiles(dir).size() == 1);
|
||||
QVERIFY(listFiles(dir, QDirIterator::Subdirectories).size() == 3);
|
||||
QVERIFY(dir.mkpath("Sub/Sub2/Sub3")); // Create an existing dir hierarchy
|
||||
QVERIFY(dir.rmdir("Sub"));
|
||||
|
||||
}
|
||||
|
||||
void tst_ContentUris::readWriteFile()
|
||||
{
|
||||
const QByteArray content = "Written to file";
|
||||
const QString fileName = "new_file.txt";
|
||||
|
||||
{
|
||||
showInstructionsDialog("Choose a name for new file to create");
|
||||
|
||||
auto url = QFileDialog::getSaveFileName(nullptr, tr("Save File"), fileName);
|
||||
QFile file(url);
|
||||
QVERIFY(file.exists());
|
||||
QVERIFY(file.size() == 0);
|
||||
QVERIFY(file.fileName() == url);
|
||||
QVERIFY(QFileInfo(url).baseName() == fileName);
|
||||
|
||||
QVERIFY(file.open(QFile::WriteOnly));
|
||||
QVERIFY(file.isOpen());
|
||||
QVERIFY(file.isWritable());
|
||||
QVERIFY(file.fileTime(QFileDevice::FileModificationTime) != QDateTime());
|
||||
QVERIFY(file.write(content) > 0);
|
||||
QVERIFY(file.size() == content.size());
|
||||
file.close();
|
||||
|
||||
// NOTE: The native file cursor is not returning an updated time or it takes long
|
||||
// for it to get updated, for now just check that we actually received a valid QDateTime
|
||||
QVERIFY(file.fileTime(QFileDevice::FileModificationTime) != QDateTime());
|
||||
}
|
||||
|
||||
{
|
||||
showInstructionsDialog("Choose the file that was created");
|
||||
|
||||
auto url = QFileDialog::getOpenFileName(nullptr, tr("Open File"), fileName);
|
||||
QFile file(url);
|
||||
QVERIFY(file.exists());
|
||||
|
||||
QVERIFY(file.open(QFile::ReadOnly));
|
||||
QVERIFY(file.isOpen());
|
||||
QVERIFY(file.isReadable());
|
||||
QVERIFY(file.readAll() == content);
|
||||
|
||||
QVERIFY(file.remove());
|
||||
}
|
||||
}
|
||||
|
||||
void tst_ContentUris::readWriteNonExistingFile_data()
|
||||
{
|
||||
QTest::addColumn<QString>("path");
|
||||
|
||||
const QString fileName = "non-existing-file.txt";
|
||||
const QString uriSchemeAuthority = "content://com.android.externalstorage.documents";
|
||||
const QString id = "primary%3APictures";
|
||||
const QString encSlash = QUrl::toPercentEncoding("/"_L1);
|
||||
|
||||
const QString docSlash = uriSchemeAuthority + "/document/"_L1 + id + "/"_L1 + fileName;
|
||||
const QString docEncodedSlash = uriSchemeAuthority + "/document/"_L1 + id + encSlash + fileName;
|
||||
|
||||
QTest::newRow("document_with_slash") << docSlash;
|
||||
QTest::newRow("document_with_encoded_slash") << docEncodedSlash;
|
||||
}
|
||||
|
||||
void tst_ContentUris::readWriteNonExistingFile()
|
||||
{
|
||||
QFETCH(QString, path);
|
||||
|
||||
QFile file(path);
|
||||
QVERIFY(!file.exists());
|
||||
QVERIFY(file.size() == 0);
|
||||
|
||||
QVERIFY(!file.open(QFile::WriteOnly));
|
||||
QVERIFY(!file.isOpen());
|
||||
QVERIFY(!file.isWritable());
|
||||
}
|
||||
|
||||
void tst_ContentUris::createFileFromDirUrl_data()
|
||||
{
|
||||
QTest::addColumn<QString>("path");
|
||||
|
||||
showInstructionsDialog("Choose a folder with no content/files/subdirs");
|
||||
|
||||
const QString treeUrl = QFileDialog::getExistingDirectory();
|
||||
const QString fileName = "text.txt";
|
||||
const QString treeSlash = treeUrl + "/"_L1 + fileName;
|
||||
QTest::newRow("tree_with_slash") << treeSlash;
|
||||
|
||||
// TODO: This is not handled at the moment
|
||||
// const QString encSlash = QUrl::toPercentEncoding("/"_L1);
|
||||
// const QString treeEncodedSlash = treeUrl + encSlash + fileName;
|
||||
// QTest::newRow("tree_with_encoded_slash") << treeEncodedSlash;
|
||||
}
|
||||
|
||||
void tst_ContentUris::createFileFromDirUrl()
|
||||
{
|
||||
QFETCH(QString, path);
|
||||
|
||||
const QByteArray content = "Written to file";
|
||||
|
||||
QFile file(path);
|
||||
QVERIFY(!file.exists());
|
||||
QVERIFY(file.size() == 0);
|
||||
|
||||
QVERIFY(file.open(QFile::WriteOnly));
|
||||
QVERIFY(file.isOpen());
|
||||
QVERIFY(file.isWritable());
|
||||
QVERIFY(file.exists());
|
||||
QVERIFY(file.write(content));
|
||||
QVERIFY(file.size() == content.size());
|
||||
file.close();
|
||||
|
||||
QVERIFY(file.open(QFile::ReadOnly));
|
||||
QVERIFY(file.isOpen());
|
||||
QVERIFY(file.isReadable());
|
||||
QVERIFY(file.readAll() == content);
|
||||
|
||||
QVERIFY(file.remove());
|
||||
}
|
||||
|
||||
void tst_ContentUris::fileOperations()
|
||||
{
|
||||
showInstructionsDialog("Choose a name for new file to create");
|
||||
|
||||
const QString fileName = "new_file.txt";
|
||||
auto url = QFileDialog::getSaveFileName(nullptr, tr("Save File"), fileName);
|
||||
QFile file(url);
|
||||
QVERIFY(file.exists());
|
||||
|
||||
QVERIFY(file.remove());
|
||||
QVERIFY(!file.exists());
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_ContentUris)
|
||||
#include "tst_content_uris.moc"
|
Loading…
Reference in New Issue
Block a user