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