Skip to content

Commit

Permalink
Added support for Android SAF
Browse files Browse the repository at this point in the history
You can now grant permission for Pegasus to access directories using
Android's Storage Access Framework.
  • Loading branch information
mmatyas committed Mar 19, 2022
1 parent 168ff5e commit d1e90e7
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import android.app.Activity;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.UriPermission;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
Expand Down Expand Up @@ -174,6 +175,23 @@ public static String[] sdcardPaths() {
}


public static String[] grantedPaths() {
List<String> paths = new ArrayList();
for (UriPermission uriperm : m_self.getContentResolver().getPersistedUriPermissions()) {
final Uri uri = uriperm.getUri();
paths.add(uri.getPath());
}
return paths.toArray(new String[paths.size()]);
}


public static void rememberGrantedPath(Uri uri) {
m_self
.getContentResolver()
.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}


public static boolean getAllStorageAccess() {
if (Build.VERSION.SDK_INT < 30)
return true;
Expand Down
27 changes: 27 additions & 0 deletions src/backend/model/internal/settings/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@
#include "Paths.h"
#include "utils/PathTools.h"

#ifdef Q_OS_ANDROID
#include "platform/AndroidHelpers.h"
#endif

#include <QCursor>
#include <QFileInfo>
#include <QGuiApplication>
#include <QSet>
#include <QTextStream>

#ifdef Q_OS_ANDROID
#include <QtAndroid>
#include <QtAndroidExtras/QAndroidIntent>
#endif


namespace {

Expand Down Expand Up @@ -191,6 +200,24 @@ void Settings::removeGameDirs(const QVariantList& idx_var_list)
emit gameDirsChanged();
}

QStringList Settings::androidGrantedDirs() const
{
#ifdef Q_OS_ANDROID
return android::granted_paths();
#else
return {};
#endif
}

void Settings::requestAndroidDir()
{
#ifdef Q_OS_ANDROID
android::request_saf_permission([this](){
emit androidDirsChanged();
});
#endif
}

void Settings::reloadProviders()
{
Log::info(LOGMSG("Reloading..."));
Expand Down
5 changes: 5 additions & 0 deletions src/backend/model/internal/settings/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Settings : public QObject {
READ verifyFiles WRITE setVerifyFiles
NOTIFY verifyFilesChanged)
Q_PROPERTY(QStringList gameDirs READ gameDirs NOTIFY gameDirsChanged)
Q_PROPERTY(QStringList androidGrantedDirs READ androidGrantedDirs NOTIFY androidDirsChanged)

QML_CONST_PROPERTY(model::KeyEditor, keyEditor)
QML_CONST_PROPERTY(model::Locales, locales)
Expand All @@ -66,13 +67,17 @@ class Settings : public QObject {
Q_INVOKABLE void addGameDir(const QString&);
Q_INVOKABLE void removeGameDirs(const QVariantList&);

QStringList androidGrantedDirs() const;
Q_INVOKABLE void requestAndroidDir();

Q_INVOKABLE void reloadProviders();

signals:
void fullscreenChanged();
void mouseSupportChanged();
void verifyFilesChanged();
void gameDirsChanged();
void androidDirsChanged();
void providerReloadingRequested();
};

Expand Down
69 changes: 53 additions & 16 deletions src/backend/platform/AndroidHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,35 @@
#include <QHash> // Required for PermissionResultMap
#include <QStandardPaths>
#include <QtAndroid>
#include <QtAndroidExtras/QAndroidIntent>
#include <QtAndroidExtras/QAndroidJniEnvironment>
#include <QtAndroidExtras/QAndroidJniObject>
#include <QUrl>


namespace {
QStringList query_string_array(const char* const method)
{
static constexpr auto JNI_SIGNATURE = "()[Ljava/lang/String;";

QAndroidJniEnvironment jni_env;
const auto jni_path_arr_raw = QAndroidJniObject::callStaticObjectMethod(android::jni_classname(), method, JNI_SIGNATURE);
const auto jni_path_arr = jni_path_arr_raw.object<jobjectArray>();
const jsize path_count = jni_env->GetArrayLength(jni_path_arr);

QStringList out;
out.reserve(path_count);

for (jsize i = 0; i < path_count; i++) {
const auto jni_path_raw = QAndroidJniObject(jni_env->GetObjectArrayElement(jni_path_arr, i));
out.append(jni_path_raw.toString()); // TODO: Qt 6 emplace_back
}

return out;
}
} // namespace


namespace android {

const char* jni_classname() {
Expand All @@ -38,28 +62,41 @@ QString primary_storage_path()
return QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).constFirst();
}

std::vector<QString> storage_paths()
QStringList storage_paths()
{
static constexpr auto JNI_METHOD = "sdcardPaths";
static constexpr auto JNI_SIGNATURE = "()[Ljava/lang/String;";
QStringList paths = query_string_array("sdcardPaths");
if (paths.empty())
paths.append(primary_storage_path()); // TODO: Qt 6 emplace_back
return paths;
}

QAndroidJniEnvironment jni_env;
const auto jni_path_arr_raw = QAndroidJniObject::callStaticObjectMethod(jni_classname(), JNI_METHOD, JNI_SIGNATURE);
const auto jni_path_arr = jni_path_arr_raw.object<jobjectArray>();
const jsize path_count = jni_env->GetArrayLength(jni_path_arr);
QStringList granted_paths()
{
return query_string_array("grantedPaths");
}

std::vector<QString> out;
out.reserve(static_cast<size_t>(path_count));
void request_saf_permission(const std::function<void()>& cb_success)
{
constexpr int REQ_OPEN_DOCUMENT_TREE = 0x1;

for (jsize i = 0; i < path_count; i++) {
const auto jni_path_raw = QAndroidJniObject(jni_env->GetObjectArrayElement(jni_path_arr, i));
out.emplace_back(jni_path_raw.toString());
}
const auto activity_cb = [&cb_success](int requestCode, int resultCode, const QAndroidJniObject& data) {
const jint RESULT_OK = QAndroidJniObject::getStaticField<jint>("android/app/Activity", "RESULT_OK");
if (requestCode != REQ_OPEN_DOCUMENT_TREE || resultCode != RESULT_OK || !data.isValid())
return;

if (out.empty())
out.emplace_back(primary_storage_path());
const QAndroidJniObject uri = data.callObjectMethod("getData", "()Landroid/net/Uri;");
if (!uri.isValid())
return;

return out;
static constexpr auto REMEMBER_FN = "rememberGrantedPath";
static constexpr auto REMEMBER_SIGN = "(Landroid/net/Uri;)V";
QAndroidJniObject::callStaticMethod<void>(jni_classname(), REMEMBER_FN, REMEMBER_SIGN, uri.object());

cb_success();
};

QAndroidIntent intent(QStringLiteral("android.intent.action.OPEN_DOCUMENT_TREE"));
QtAndroid::startActivity(intent.handle(), REQ_OPEN_DOCUMENT_TREE, activity_cb);
}

bool has_external_storage_access()
Expand Down
8 changes: 6 additions & 2 deletions src/backend/platform/AndroidHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
#pragma once

#include <QString>
#include <vector>


namespace android {

const char* jni_classname();

QString primary_storage_path();
std::vector<QString> storage_paths();
QStringList storage_paths();
bool has_external_storage_access();

QStringList granted_paths();
void request_saf_permission(const std::function<void()>&);

QString run_am_call(const QStringList&);
QString to_content_uri(const QString&);
QString to_document_uri(const QString&);
Expand Down
10 changes: 5 additions & 5 deletions src/backend/utils/FolderListModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void erase_if(QFileInfoList& list, const std::function<bool(const QFileInfo&)>&
list.erase(start_it, list.end());
}

std::vector<QString> drives()
QStringList drives()
{
#if defined(Q_OS_ANDROID)
return android::storage_paths();
Expand All @@ -43,11 +43,11 @@ std::vector<QString> drives()
return { QStringLiteral("/") };

#else
std::vector<QString> out;
QStringList out;

const QFileInfoList drive_files = QDir::drives();
for (const QFileInfo& file : drive_files)
out.emplace_back(::pretty_dir(file));
out.append(::pretty_dir(file)); // TODO: Qt 6 emplace_back

return out;
#endif
Expand All @@ -63,9 +63,9 @@ QDir startup_dir()
#endif
}

bool is_drive_root(const QString& path, const std::vector<QString>& drives)
bool is_drive_root(const QString& path, const QStringList& drives)
{
return VEC_CONTAINS(drives, path);
return drives.contains(path);
}
} // namespace

Expand Down
2 changes: 1 addition & 1 deletion src/backend/utils/FolderListModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,6 @@ class FolderListModel : public QAbstractListModel {
QStringList m_filenames;
QStringList m_extensions;

const std::vector<QString> m_drives_cache;
const QStringList m_drives_cache;
const QHash<int, QByteArray> m_role_names;
};
2 changes: 2 additions & 0 deletions src/frontend/frontend.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,7 @@
<file>menu/settings/gamepad/GamepadFooter.qml</file>
<file>menu/settings/gamepad/GamepadHeader.qml</file>
<file>menu/settings/gamedireditor/GameDirEditorEntry.qml</file>
<file>menu/settings/AndroidSafEditor.qml</file>
<file>menu/settings/common/SimpleShade.qml</file>
</qresource>
</RCC>
Loading

0 comments on commit d1e90e7

Please sign in to comment.