Skip to content

Commit

Permalink
[Android] Properly get the basename of files
Browse files Browse the repository at this point in the history
Use the ContentResolver API to get the display name for content: URIs.

Remaining TODO: Add refreshing to other uses of Emu.basename as well.
  • Loading branch information
Vogtinator committed Oct 18, 2024
1 parent 5b2a1a3 commit 26e7075
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 11 deletions.
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

0 comments on commit 26e7075

Please sign in to comment.