Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] Properly get the basename of files #355

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions core/os/os-android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
#include <QAndroidJniObject>
#include <QAndroidJniEnvironment>
#include <QAndroidActivityResultReceiver>
#include <QDebug>
#include <QScopeGuard>
#include <QUrl>

#include "os.h"

static bool is_content_url(const char *path)
{
const char pattern[] = "content:";
return strncmp(pattern, path, sizeof(pattern) - 1) == 0;
}

// A handler to open android content:// URLs.
// Based on code by Florin9doi: https://github.com/nspire-emus/firebird/pull/94/files
FILE *fopen_utf8(const char *path, const char *mode)
{
const char pattern[] = "content:";
if(strncmp(pattern, path, sizeof(pattern)-1) != 0)
if(!is_content_url(path))
return fopen(path, mode);

QString android_mode; // Why did they have to NIH...
Expand Down Expand Up @@ -78,3 +86,72 @@ FILE *fopen_utf8(const char *path, const char *mode)

return fdopen(fd, mode);
}

static QString android_basename_using_content_resolver(const QString &path)
{
QAndroidJniObject jpath = QAndroidJniObject::fromString(path);
QAndroidJniObject uri = QAndroidJniObject::callStaticObjectMethod(
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
jpath.object<jstring>());

QAndroidJniObject contentResolver = QtAndroid::androidActivity()
.callObjectMethod("getContentResolver",
"()Landroid/content/ContentResolver;");

QAndroidJniEnvironment env;
QAndroidJniObject col = QAndroidJniObject::getStaticObjectField("android/provider/OpenableColumns", "DISPLAY_NAME", "Ljava/lang/String;");
QAndroidJniObject proj = env->NewObjectArray(1, env->FindClass("java/lang/String"), col.object<jstring>());

QAndroidJniObject cursor = contentResolver.callObjectMethod("query", "(Landroid/net/Uri;[Ljava/lang/String;Landroid/os/Bundle;Landroid/os/CancellationSignal;)Landroid/database/Cursor;", uri.object<jobject>(), proj.object<jobject>(), nullptr, nullptr);
if (env->ExceptionCheck())
{
env->ExceptionDescribe();
env->ExceptionClear();
return {};
}

if(!cursor.isValid())
return {};

auto closeCursor = qScopeGuard([&] { cursor.callMethod<void>("close", "()V"); });

bool hasContent = cursor.callMethod<jboolean>("moveToFirst", "()Z");
if (env->ExceptionCheck())
{
env->ExceptionDescribe();
env->ExceptionClear();
return {};
}

if(!hasContent)
return {};

QAndroidJniObject name = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", 0);
if (!name.isValid())
return {};

return name.toString();
}

char *android_basename(const char *path)
{
if (is_content_url(path))
{
// Example: content://com.android.externalstorage.documents/document/primary%3AFirebird%2Fflash_tpad
QString pathStr = QString::fromUtf8(path);
QString ret = android_basename_using_content_resolver(pathStr);
// If that failed (e.g. because the permission expired), try to get something recognizable.
if (ret.isEmpty())
{
qWarning() << "Failed to get basename of" << pathStr << "using ContentResolver";
auto parts = pathStr.splitRef(QStringLiteral("%2F"), QString::SkipEmptyParts, Qt::CaseInsensitive);
if(parts.length() > 1)
ret = QUrl::fromPercentEncoding(parts.last().toString().toUtf8());
}

if (!ret.isEmpty())
return strdup(ret.toUtf8().data());
}

return nullptr;
}
5 changes: 5 additions & 0 deletions core/os/os.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ int iOS_is_debugger_attached();
/* Some really crappy APIs don't use UTF-8 in fopen. */
FILE *fopen_utf8(const char *filename, const char *mode);

#if defined(__ANDROID__)
/* Returns an allocated string or NULL on failure. */
char *android_basename(const char *path);
#endif

void *os_reserve(size_t size);
void *os_alloc_executable(size_t size);
void os_free(void *ptr, size_t size);
Expand Down
2 changes: 1 addition & 1 deletion qml/Firebird/UIComponents/FileSelect.qml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ RowLayout {
Layout.preferredWidth: 100

font.italic: filePath === ""
text: filePath === "" ? qsTr("(none)") : Emu.basename(filePath)
text: { forceRefresh; return filePath === "" ? qsTr("(none)") : Emu.basename(filePath); }
color: { forceRefresh; return ((!selectExisting && Emu.saveDialogSupported()) || filePath === "" || Emu.fileExists(filePath)) ? paletteActive.text : "red"; }
}

Expand Down
13 changes: 5 additions & 8 deletions qmlbridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,11 @@ QString QMLBridge::basename(QString path)
if(path.isEmpty())
return tr("None");

if(path.startsWith(QStringLiteral("content://")))
{
auto parts = path.splitRef(QStringLiteral("%2F"), QString::SkipEmptyParts, Qt::CaseInsensitive);
if(parts.length() > 1)
return parts.last().toString();

return tr("(Android File)");
}
#ifdef Q_OS_ANDROID
QScopedPointer<char, QScopedPointerPodDeleter> android_bn{android_basename(path.toUtf8().data())};
if(android_bn)
return QString::fromUtf8(android_bn.data());
#endif

return QFileInfo(path).fileName();
}
Expand Down
Loading