Android: Support custom FileProvider in openUrl

Previously, qtprovider was set as the default FileProvider, which
causes issues when creating a custom FileProvider on application side or
having multiple FileProviders.
To address this problem, we have implemented a solution that checks for
all available FileProviders and ,in case of multiple FileProviders,
selects first non-default FileProvider.

Fixes: QTBUG-117417
Pick-to: 6.7 6.5
Change-Id: I2a68983403f964036dc3177e13fe7ea4f9b4788b
Reviewed-by: Assam Boudjelthia <assam.boudjelthia@qt.io>
(cherry picked from commit d3508d48c0e4ef73a3a673ea14fb59cd8d930398)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Ahmed El Khazari 2024-07-03 14:13:49 +03:00 committed by Qt Cherry-pick Bot
parent 39524fabc4
commit a96e0eddd9
2 changed files with 94 additions and 28 deletions

View File

@ -14,8 +14,12 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
using namespace QtJniTypes;
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
static constexpr auto s_defaultScheme = "file"_L1;
static constexpr auto s_defaultProvider = "qtprovider"_L1;
QAndroidPlatformServices::QAndroidPlatformServices() QAndroidPlatformServices::QAndroidPlatformServices()
{ {
m_actionView = QJniObject::getStaticObjectField("android/content/Intent", "ACTION_VIEW", m_actionView = QJniObject::getStaticObjectField("android/content/Intent", "ACTION_VIEW",
@ -39,57 +43,110 @@ QAndroidPlatformServices::QAndroidPlatformServices()
} }
Q_DECLARE_JNI_CLASS(FileProvider, "androidx/core/content/FileProvider"); Q_DECLARE_JNI_CLASS(FileProvider, "androidx/core/content/FileProvider");
Q_DECLARE_JNI_CLASS(PackageManager, "android/content/pm/PackageManager");
Q_DECLARE_JNI_CLASS(PackageInfo, "android/content/pm/PackageInfo");
Q_DECLARE_JNI_CLASS(ProviderInfo, "android/content/pm/ProviderInfo");
bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) bool QAndroidPlatformServices::openUrl(const QUrl &theUrl)
{ {
QString mime;
QUrl url(theUrl); QUrl url(theUrl);
// avoid recursing back into self // avoid recursing back into self
if (url == m_handlingUrl) if (url == m_handlingUrl)
return false; return false;
// if the file is local, we need to pass the MIME type, otherwise Android
// does not start an Intent to view this file
const auto fileScheme = "file"_L1;
// a real URL including the scheme is needed, else the Intent can not be started // a real URL including the scheme is needed, else the Intent can not be started
if (url.scheme().isEmpty()) if (url.scheme().isEmpty())
url.setScheme(fileScheme); url.setScheme(s_defaultScheme);
if (url.scheme() == fileScheme) const int sdkVersion = QNativeInterface::QAndroidApplication::sdkVersion();
if (url.scheme() != s_defaultScheme || sdkVersion < 24 )
return openURL(url);
return openUrlWithFileProvider(url);
}
QString QAndroidPlatformServices::getMimeOfUrl(const QUrl &url) const
{
QString mime;
if (url.scheme() == s_defaultScheme)
mime = QMimeDatabase().mimeTypeForUrl(url).name(); mime = QMimeDatabase().mimeTypeForUrl(url).name();
return mime;
}
const QJniObject mimeString = QJniObject::fromString(mime); bool QAndroidPlatformServices::openURL(const QUrl &url) const
{
using namespace QNativeInterface; return QJniObject::callStaticMethod<jboolean>(
QtAndroid::applicationClass(), "openURL",
auto openUrl = [mimeString](const QJniObject &url) { QNativeInterface::QAndroidApplication::context(),
return QJniObject::callStaticMethod<jboolean>(QtAndroid::applicationClass(), "openURL", url.toString(),
QAndroidApplication::context(), url.object<jstring>(), mimeString.object<jstring>()); getMimeOfUrl(url));
}; }
if (url.scheme() != fileScheme || QNativeInterface::QAndroidApplication::sdkVersion() < 24)
return openUrl(QJniObject::fromString(url.toString()));
// Use FileProvider for file scheme with sdk >= 24
const QJniObject context = QAndroidApplication::context();
const auto appId = context.callMethod<jstring>("getPackageName").toString();
const auto providerName = QJniObject::fromString(appId + ".qtprovider"_L1);
const auto urlPath = QJniObject::fromString(url.path());
const auto urlFile = QJniObject(QtJniTypes::Traits<QtJniTypes::File>::className(),
urlPath.object<jstring>());
const auto fileProviderUri = QJniObject::callStaticMethod<QtJniTypes::Uri>(
QtJniTypes::Traits<QtJniTypes::FileProvider>::className(), "getUriForFile",
QAndroidApplication::context(), providerName.object<jstring>(),
urlFile.object<QtJniTypes::File>());
if (fileProviderUri.isValid())
return openUrl(fileProviderUri.callMethod<jstring>("toString"));
bool QAndroidPlatformServices::openUrlWithFileProvider(const QUrl &url)
{
const QJniObject context = QNativeInterface::QAndroidApplication::context();
auto authorities = getFileProviderAuthorities(context);
if (authorities.isEmpty())
return false; return false;
return openUrlWithAuthority(url, getAdequateFileproviderAuthority(authorities));
}
QString QAndroidPlatformServices::getAdequateFileproviderAuthority(const QStringList &authorities) const
{
if (authorities.size() == 1)
return authorities[0];
QString nonQtAuthority;
for (const auto &authority : authorities) {
if (!authority.endsWith(s_defaultProvider, Qt::CaseSensitive)) {
nonQtAuthority = authority;
break;
}
}
return nonQtAuthority;
}
bool QAndroidPlatformServices::openUrlWithAuthority(const QUrl &url, const QString &authority)
{
const auto urlPath = QJniObject::fromString(url.path());
const auto urlFile = QJniObject(Traits<File>::className(),
urlPath.object<jstring>());
const auto fileProviderUri = QJniObject::callStaticMethod<Uri>(
Traits<FileProvider>::className(), "getUriForFile",
QNativeInterface::QAndroidApplication::context(), authority,
urlFile.object<File>());
if (fileProviderUri.isValid())
return openURL(url);
return false;
}
QStringList QAndroidPlatformServices::getFileProviderAuthorities(const QJniObject &context) const
{
QStringList authorityList;
const auto packageManager = context.callMethod<PackageManager>("getPackageManager");
const auto packageName = context.callMethod<QString>("getPackageName");
const auto packageInfo = packageManager.callMethod<PackageInfo>("getPackageInfo",
packageName,
8 /* PackageManager.GET_PROVIDERS */);
const auto providersArray = packageInfo.getField<ProviderInfo[]>("providers");
if (providersArray.isValid()) {
const auto className = Traits<FileProvider>::className();
for (const auto &fileProvider : providersArray) {
auto providerName = fileProvider.getField<QString>("name");
if (providerName.replace(".", "/").contains(className.data())) {
const auto authority = fileProvider.getField<QString>("authority");
if (!authority.isEmpty())
authorityList << authority;
}
}
}
if (authorityList.isEmpty())
qWarning() << "No file provider found in the AndroidManifest.xml.";
return authorityList;
} }
bool QAndroidPlatformServices::openDocument(const QUrl &url) bool QAndroidPlatformServices::openDocument(const QUrl &url)

View File

@ -25,6 +25,15 @@ public:
bool handleNewIntent(JNIEnv *env, jobject intent) override; bool handleNewIntent(JNIEnv *env, jobject intent) override;
private:
bool openURL(const QUrl &url) const;
bool openUrlWithFileProvider(const QUrl &url);
bool openUrlWithAuthority(const QUrl &url, const QString &authority);
QString getMimeOfUrl(const QUrl &url) const;
QStringList getFileProviderAuthorities(const QJniObject &context) const;
QString getAdequateFileproviderAuthority(const QStringList &authorities) const;
private: private:
QUrl m_handlingUrl; QUrl m_handlingUrl;
QString m_actionView; QString m_actionView;