1 /****************************************************************************
2 **
3 ** Copyright (C) 2019 Klaralvdalens Datakonsult AB (KDAB)
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the plugins of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39
40 #include "qandroidplatformfiledialoghelper.h"
41
42 #include <androidjnimain.h>
43 #include <jni.h>
44
45 #include <QMimeType>
46 #include <QMimeDatabase>
47 #include <QRegularExpression>
48
49 QT_BEGIN_NAMESPACE
50
51 namespace QtAndroidFileDialogHelper {
52
53 #define RESULT_OK -1
54 #define REQUEST_CODE 1305 // Arbitrary
55
56 const char JniIntentClass[] = "android/content/Intent";
57
QAndroidPlatformFileDialogHelper()58 QAndroidPlatformFileDialogHelper::QAndroidPlatformFileDialogHelper()
59 : QPlatformFileDialogHelper(),
60 m_activity(QtAndroid::activity())
61 {
62 }
63
handleActivityResult(jint requestCode,jint resultCode,jobject data)64 bool QAndroidPlatformFileDialogHelper::handleActivityResult(jint requestCode, jint resultCode, jobject data)
65 {
66 if (requestCode != REQUEST_CODE)
67 return false;
68
69 if (resultCode != RESULT_OK) {
70 Q_EMIT reject();
71 return true;
72 }
73
74 const QJNIObjectPrivate intent = QJNIObjectPrivate::fromLocalRef(data);
75
76 const QJNIObjectPrivate uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;");
77 if (uri.isValid()) {
78 takePersistableUriPermission(uri);
79 m_selectedFile.append(QUrl(uri.toString()));
80 Q_EMIT fileSelected(m_selectedFile.first());
81 Q_EMIT accept();
82
83 return true;
84 }
85
86 const QJNIObjectPrivate uriClipData =
87 intent.callObjectMethod("getClipData", "()Landroid/content/ClipData;");
88 if (uriClipData.isValid()) {
89 const int size = uriClipData.callMethod<jint>("getItemCount");
90 for (int i = 0; i < size; ++i) {
91 QJNIObjectPrivate item = uriClipData.callObjectMethod(
92 "getItemAt", "(I)Landroid/content/ClipData$Item;", i);
93
94 QJNIObjectPrivate itemUri = item.callObjectMethod("getUri", "()Landroid/net/Uri;");
95 takePersistableUriPermission(itemUri);
96 m_selectedFile.append(itemUri.toString());
97 }
98 Q_EMIT filesSelected(m_selectedFile);
99 Q_EMIT accept();
100 }
101
102 return true;
103 }
104
takePersistableUriPermission(const QJNIObjectPrivate & uri)105 void QAndroidPlatformFileDialogHelper::takePersistableUriPermission(const QJNIObjectPrivate &uri)
106 {
107 int modeFlags = QJNIObjectPrivate::getStaticField<jint>(
108 JniIntentClass, "FLAG_GRANT_READ_URI_PERMISSION");
109
110 if (options()->acceptMode() == QFileDialogOptions::AcceptSave) {
111 modeFlags |= QJNIObjectPrivate::getStaticField<jint>(
112 JniIntentClass, "FLAG_GRANT_WRITE_URI_PERMISSION");
113 }
114
115 QJNIObjectPrivate contentResolver = m_activity.callObjectMethod(
116 "getContentResolver", "()Landroid/content/ContentResolver;");
117 contentResolver.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V",
118 uri.object(), modeFlags);
119 }
120
setIntentTitle(const QString & title)121 void QAndroidPlatformFileDialogHelper::setIntentTitle(const QString &title)
122 {
123 const QJNIObjectPrivate extraTitle = QJNIObjectPrivate::getStaticObjectField(
124 JniIntentClass, "EXTRA_TITLE", "Ljava/lang/String;");
125 m_intent.callObjectMethod("putExtra",
126 "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
127 extraTitle.object(), QJNIObjectPrivate::fromString(title).object());
128 }
129
setOpenableCategory()130 void QAndroidPlatformFileDialogHelper::setOpenableCategory()
131 {
132 const QJNIObjectPrivate CATEGORY_OPENABLE = QJNIObjectPrivate::getStaticObjectField(
133 JniIntentClass, "CATEGORY_OPENABLE", "Ljava/lang/String;");
134 m_intent.callObjectMethod("addCategory", "(Ljava/lang/String;)Landroid/content/Intent;",
135 CATEGORY_OPENABLE.object());
136 }
137
setAllowMultipleSelections(bool allowMultiple)138 void QAndroidPlatformFileDialogHelper::setAllowMultipleSelections(bool allowMultiple)
139 {
140 const QJNIObjectPrivate allowMultipleSelections = QJNIObjectPrivate::getStaticObjectField(
141 JniIntentClass, "EXTRA_ALLOW_MULTIPLE", "Ljava/lang/String;");
142 m_intent.callObjectMethod("putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;",
143 allowMultipleSelections.object(), allowMultiple);
144 }
145
nameFilterExtensions(const QString nameFilters)146 QStringList nameFilterExtensions(const QString nameFilters)
147 {
148 QStringList ret;
149 #if QT_CONFIG(regularexpression)
150 QRegularExpression re("(\\*\\.?\\w*)");
151 QRegularExpressionMatchIterator i = re.globalMatch(nameFilters);
152 while (i.hasNext())
153 ret << i.next().captured(1);
154 #endif // QT_CONFIG(regularexpression)
155 ret.removeAll("*");
156 return ret;
157 }
158
setMimeTypes()159 void QAndroidPlatformFileDialogHelper::setMimeTypes()
160 {
161 QStringList mimeTypes = options()->mimeTypeFilters();
162 const QString nameFilter = options()->initiallySelectedNameFilter();
163
164 if (mimeTypes.isEmpty() && !nameFilter.isEmpty()) {
165 QMimeDatabase db;
166 for (const QString &filter : nameFilterExtensions(nameFilter))
167 mimeTypes.append(db.mimeTypeForFile(filter).name());
168 }
169
170 QString type = !mimeTypes.isEmpty() ? mimeTypes.at(0) : QLatin1String("*/*");
171 m_intent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;",
172 QJNIObjectPrivate::fromString(type).object());
173
174 if (!mimeTypes.isEmpty()) {
175 const QJNIObjectPrivate extraMimeType = QJNIObjectPrivate::getStaticObjectField(
176 JniIntentClass, "EXTRA_MIME_TYPES", "Ljava/lang/String;");
177
178 QJNIObjectPrivate mimeTypesArray = QJNIObjectPrivate::callStaticObjectMethod(
179 "org/qtproject/qt5/android/QtNative",
180 "getStringArray",
181 "(Ljava/lang/String;)[Ljava/lang/String;",
182 QJNIObjectPrivate::fromString(mimeTypes.join(",")).object());
183
184 m_intent.callObjectMethod(
185 "putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;",
186 extraMimeType.object(), mimeTypesArray.object());
187 }
188 }
189
getFileDialogIntent(const QString & intentType)190 QJNIObjectPrivate QAndroidPlatformFileDialogHelper::getFileDialogIntent(const QString &intentType)
191 {
192 const QJNIObjectPrivate ACTION_OPEN_DOCUMENT = QJNIObjectPrivate::getStaticObjectField(
193 JniIntentClass, intentType.toLatin1(), "Ljava/lang/String;");
194 return QJNIObjectPrivate(JniIntentClass, "(Ljava/lang/String;)V",
195 ACTION_OPEN_DOCUMENT.object());
196 }
197
show(Qt::WindowFlags windowFlags,Qt::WindowModality windowModality,QWindow * parent)198 bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
199 {
200 Q_UNUSED(windowFlags)
201 Q_UNUSED(windowModality)
202 Q_UNUSED(parent)
203
204 bool isDirDialog = false;
205
206 m_selectedFile.clear();
207
208 if (options()->acceptMode() == QFileDialogOptions::AcceptSave) {
209 m_intent = getFileDialogIntent("ACTION_CREATE_DOCUMENT");
210 } else if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) {
211 switch (options()->fileMode()) {
212 case QFileDialogOptions::FileMode::DirectoryOnly:
213 case QFileDialogOptions::FileMode::Directory:
214 m_intent = getFileDialogIntent("ACTION_OPEN_DOCUMENT_TREE");
215 isDirDialog = true;
216 break;
217 case QFileDialogOptions::FileMode::ExistingFiles:
218 m_intent = getFileDialogIntent("ACTION_OPEN_DOCUMENT");
219 setAllowMultipleSelections(true);
220 break;
221 case QFileDialogOptions::FileMode::AnyFile:
222 case QFileDialogOptions::FileMode::ExistingFile:
223 m_intent = getFileDialogIntent("ACTION_OPEN_DOCUMENT");
224 break;
225 }
226 }
227
228 if (!isDirDialog) {
229 setOpenableCategory();
230 setMimeTypes();
231 }
232
233 setIntentTitle(options()->windowTitle());
234
235 QtAndroidPrivate::registerActivityResultListener(this);
236 m_activity.callMethod<void>("startActivityForResult", "(Landroid/content/Intent;I)V",
237 m_intent.object(), REQUEST_CODE);
238 return true;
239 }
240
hide()241 void QAndroidPlatformFileDialogHelper::hide()
242 {
243 if (m_eventLoop.isRunning())
244 m_eventLoop.exit();
245 QtAndroidPrivate::unregisterActivityResultListener(this);
246 }
247
exec()248 void QAndroidPlatformFileDialogHelper::exec()
249 {
250 m_eventLoop.exec(QEventLoop::DialogExec);
251 }
252 }
253
254 QT_END_NAMESPACE
255