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.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
@ -83,8 +82,6 @@ public class QtNative
|
|||||||
private static Boolean m_tabletEventSupported = null;
|
private static Boolean m_tabletEventSupported = null;
|
||||||
private static boolean m_usePrimaryClip = false;
|
private static boolean m_usePrimaryClip = false;
|
||||||
public static QtThread m_qtThread = new QtThread();
|
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 int KEYBOARD_HEIGHT_THRESHOLD = 100;
|
||||||
|
|
||||||
private static final String INVALID_OR_NULL_URI_ERROR_MESSAGE = "Received invalid/null Uri";
|
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
|
// this method loads full path libs
|
||||||
public static void loadQtLibraries(final ArrayList<String> libraries)
|
public static void loadQtLibraries(final ArrayList<String> libraries)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
|
// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
|
||||||
// Copyright (C) 2021 The Qt Company Ltd.
|
// 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
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||||
|
|
||||||
#include "androidcontentfileengine.h"
|
#include "androidcontentfileengine.h"
|
||||||
@ -7,16 +7,29 @@
|
|||||||
#include <QtCore/qcoreapplication.h>
|
#include <QtCore/qcoreapplication.h>
|
||||||
#include <QtCore/qjnienvironment.h>
|
#include <QtCore/qjnienvironment.h>
|
||||||
#include <QtCore/qjniobject.h>
|
#include <QtCore/qjniobject.h>
|
||||||
|
#include <QtCore/qurl.h>
|
||||||
#include <QDebug>
|
#include <QtCore/qdatetime.h>
|
||||||
|
#include <QtCore/qmimedatabase.h>
|
||||||
|
|
||||||
using namespace QNativeInterface;
|
using namespace QNativeInterface;
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
|
||||||
AndroidContentFileEngine::AndroidContentFileEngine(const QString &f)
|
static QJniObject &contentResolverInstance()
|
||||||
: m_file(f)
|
|
||||||
{
|
{
|
||||||
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,
|
bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
||||||
@ -29,6 +42,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
|||||||
}
|
}
|
||||||
if (openMode & QFileDevice::WriteOnly) {
|
if (openMode & QFileDevice::WriteOnly) {
|
||||||
openModeStr += u'w';
|
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) {
|
if (openMode & QFileDevice::Truncate) {
|
||||||
openModeStr += u't';
|
openModeStr += u't';
|
||||||
@ -36,12 +70,10 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
|||||||
openModeStr += u'a';
|
openModeStr += u'a';
|
||||||
}
|
}
|
||||||
|
|
||||||
m_pfd = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative",
|
m_pfd = contentResolverInstance().callObjectMethod("openFileDescriptor",
|
||||||
"openParcelFdForContentUrl",
|
"(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;",
|
||||||
"(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;",
|
m_documentFile->uri().object(),
|
||||||
QAndroidApplication::context(),
|
QJniObject::fromString(openModeStr).object());
|
||||||
QJniObject::fromString(fileName(DefaultName)).object(),
|
|
||||||
QJniObject::fromString(openModeStr).object());
|
|
||||||
|
|
||||||
if (!m_pfd.isValid())
|
if (!m_pfd.isValid())
|
||||||
return false;
|
return false;
|
||||||
@ -49,8 +81,7 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
|||||||
const auto fd = m_pfd.callMethod<jint>("getFd", "()I");
|
const auto fd = m_pfd.callMethod<jint>("getFd", "()I");
|
||||||
|
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
m_pfd.callMethod<void>("close", "()V");
|
closeNativeFileDescriptor();
|
||||||
m_pfd = QJniObject();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,48 +89,122 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool AndroidContentFileEngine::close()
|
bool AndroidContentFileEngine::close()
|
||||||
|
{
|
||||||
|
closeNativeFileDescriptor();
|
||||||
|
return QFSFileEngine::close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidContentFileEngine::closeNativeFileDescriptor()
|
||||||
{
|
{
|
||||||
if (m_pfd.isValid()) {
|
if (m_pfd.isValid()) {
|
||||||
m_pfd.callMethod<void>("close", "()V");
|
m_pfd.callMethod<void>("close", "()V");
|
||||||
m_pfd = QJniObject();
|
m_pfd = QJniObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
return QFSFileEngine::close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qint64 AndroidContentFileEngine::size() const
|
qint64 AndroidContentFileEngine::size() const
|
||||||
{
|
{
|
||||||
const jlong size = QJniObject::callStaticMethod<jlong>(
|
return m_documentFile->length();
|
||||||
"org/qtproject/qt/android/QtNative", "getSize",
|
}
|
||||||
"(Landroid/content/Context;Ljava/lang/String;)J", QAndroidApplication::context(),
|
|
||||||
QJniObject::fromString(fileName(DefaultName)).object());
|
bool AndroidContentFileEngine::remove()
|
||||||
return (qint64)size;
|
{
|
||||||
|
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
|
AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlags type) const
|
||||||
{
|
{
|
||||||
FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag);
|
|
||||||
FileFlags flags;
|
FileFlags flags;
|
||||||
const bool isDir = QJniObject::callStaticMethod<jboolean>(
|
if (!m_documentFile->exists())
|
||||||
"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)
|
|
||||||
return flags;
|
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 {
|
} else {
|
||||||
flags = FileType | commonFlags;
|
flags |= FileType;
|
||||||
const bool writable = QJniObject::callStaticMethod<jboolean>(
|
if (m_documentFile->canWrite())
|
||||||
"org/qtproject/qt/android/QtNative", "checkIfWritable",
|
|
||||||
"(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(),
|
|
||||||
QJniObject::fromString(fileName(DefaultName)).object());
|
|
||||||
if (writable)
|
|
||||||
flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm;
|
flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm;
|
||||||
}
|
}
|
||||||
return type & flags;
|
return type & flags;
|
||||||
@ -114,18 +219,18 @@ QString AndroidContentFileEngine::fileName(FileName f) const
|
|||||||
case DefaultName:
|
case DefaultName:
|
||||||
case AbsoluteName:
|
case AbsoluteName:
|
||||||
case CanonicalName:
|
case CanonicalName:
|
||||||
return m_file;
|
return m_documentFile->uri().toString();
|
||||||
case BaseName:
|
case BaseName:
|
||||||
{
|
return m_documentFile->name();
|
||||||
const qsizetype pos = m_file.lastIndexOf(u'/');
|
|
||||||
return m_file.mid(pos);
|
|
||||||
}
|
|
||||||
default:
|
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);
|
return new AndroidContentFileEngineIterator(filters, filterNames);
|
||||||
}
|
}
|
||||||
@ -166,42 +271,468 @@ QString AndroidContentFileEngineIterator::next()
|
|||||||
|
|
||||||
bool AndroidContentFileEngineIterator::hasNext() const
|
bool AndroidContentFileEngineIterator::hasNext() const
|
||||||
{
|
{
|
||||||
if (m_index == -1) {
|
if (m_index == -1 && m_files.isEmpty()) {
|
||||||
if (path().isEmpty())
|
const auto currentPath = path();
|
||||||
|
if (currentPath.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
const bool isDir = QJniObject::callStaticMethod<jboolean>(
|
|
||||||
"org/qtproject/qt/android/QtNative", "checkIfDir",
|
const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath);
|
||||||
"(Landroid/content/Context;Ljava/lang/String;)Z",
|
if (iterDoc->isDirectory())
|
||||||
QAndroidApplication::context(),
|
for (const auto &doc : iterDoc->listFiles())
|
||||||
QJniObject::fromString(path()).object());
|
m_files.append(doc);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return m_index < m_entries.size();
|
|
||||||
|
return m_index < (m_files.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AndroidContentFileEngineIterator::currentFileName() const
|
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 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
|
QString AndroidContentFileEngineIterator::currentFilePath() const
|
||||||
{
|
{
|
||||||
return currentFileName();
|
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
|
#define ANDROIDCONTENTFILEENGINE_H
|
||||||
|
|
||||||
#include <private/qfsfileengine_p.h>
|
#include <private/qfsfileengine_p.h>
|
||||||
|
|
||||||
#include <QtCore/qjniobject.h>
|
#include <QtCore/qjniobject.h>
|
||||||
|
#include <QtCore/qlist.h>
|
||||||
|
|
||||||
|
using DocumentFilePtr = std::shared_ptr<class DocumentFile>;
|
||||||
|
|
||||||
class AndroidContentFileEngine : public QFSFileEngine
|
class AndroidContentFileEngine : public QFSFileEngine
|
||||||
{
|
{
|
||||||
@ -14,14 +18,24 @@ public:
|
|||||||
bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override;
|
bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override;
|
||||||
bool close() override;
|
bool close() override;
|
||||||
qint64 size() const 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;
|
FileFlags fileFlags(FileFlags type = FileInfoAll) const override;
|
||||||
QString fileName(FileName file = DefaultName) const override;
|
QString fileName(FileName file = DefaultName) const override;
|
||||||
QAbstractFileEngine::Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override;
|
QAbstractFileEngine::Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override;
|
||||||
QAbstractFileEngine::Iterator *endEntryList() 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
|
class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler
|
||||||
@ -42,8 +56,48 @@ public:
|
|||||||
QString currentFileName() const override;
|
QString currentFileName() const override;
|
||||||
QString currentFilePath() const override;
|
QString currentFilePath() const override;
|
||||||
private:
|
private:
|
||||||
mutable QStringList m_entries;
|
mutable QList<DocumentFilePtr> m_files;
|
||||||
mutable int m_index = -1;
|
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
|
#endif // ANDROIDCONTENTFILEENGINE_H
|
||||||
|
@ -89,3 +89,7 @@ endif()
|
|||||||
if(QT_FEATURE_vulkan)
|
if(QT_FEATURE_vulkan)
|
||||||
add_subdirectory(qvulkaninstance)
|
add_subdirectory(qvulkaninstance)
|
||||||
endif()
|
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