Warning, /graphics/krita/3rdparty/ext_qt/0044-Android-Rework-Storage-Access-Framework.patch is written in an unsupported language. File is not indexed.
0001 From 4843173fe19d32942a6badf3ba08265ac6ff15a1 Mon Sep 17 00:00:00 2001 0002 From: Sharaf Zaman <shzam@sdf.org> 0003 Date: Fri, 26 Nov 2021 15:56:43 +0000 0004 Subject: Android: Rework Storage Access Framework 0005 0006 With this commit we introduce creation of directories, files, and files 0007 inside directories. We also changed the permission model of accessing 0008 the files. 0009 --- 0010 mkspecs/features/java.prf | 2 +- 0011 src/android/jar/AndroidManifest.xml | 1 - 0012 src/android/jar/jar.pro | 3 + 0013 .../qt5/android/CachedDocumentFile.java | 225 +++++ 0014 .../org/qtproject/qt5/android/QtNative.java | 253 +----- 0015 .../org/qtproject/qt5/android/SAFFile.java | 23 + 0016 .../qtproject/qt5/android/SAFFileManager.java | 800 ++++++++++++++++++ 0017 .../org/qtproject/qt5/android/SAFUtils.java | 23 + 0018 .../android/androidcontentfileengine.cpp | 268 ++++-- 0019 .../android/androidcontentfileengine.h | 39 +- 0020 .../qandroidplatformfiledialoghelper.cpp | 6 + 0021 .../android/qandroidplatformservices.cpp | 11 +- 0022 12 files changed, 1354 insertions(+), 300 deletions(-) 0023 create mode 100644 src/android/jar/src/org/qtproject/qt5/android/CachedDocumentFile.java 0024 create mode 100644 src/android/jar/src/org/qtproject/qt5/android/SAFFile.java 0025 create mode 100644 src/android/jar/src/org/qtproject/qt5/android/SAFFileManager.java 0026 create mode 100644 src/android/jar/src/org/qtproject/qt5/android/SAFUtils.java 0027 0028 diff --git a/mkspecs/features/java.prf b/mkspecs/features/java.prf 0029 index f1f5e4c10c..1d52f05e52 100644 0030 --- a/mkspecs/features/java.prf 0031 +++ b/mkspecs/features/java.prf 0032 @@ -20,7 +20,7 @@ CONFIG += plugin no_plugin_name_prefix 0033 javac.input = JAVASOURCES 0034 javac.output = $$CLASS_DIR 0035 javac.CONFIG += combine 0036 -javac.commands = javac -source 6 -target 6 -Xlint:unchecked -bootclasspath $$ANDROID_JAR_FILE -cp $$shell_quote($$system_path($$join(JAVACLASSPATH, $$DIRLIST_SEPARATOR))) -d $$shell_quote($$CLASS_DIR) ${QMAKE_FILE_IN} 0037 +javac.commands = javac -source 7 -target 7 -Xlint:unchecked -bootclasspath $$ANDROID_JAR_FILE -cp $$shell_quote($$system_path($$join(JAVACLASSPATH, $$DIRLIST_SEPARATOR))) -d $$shell_quote($$CLASS_DIR) ${QMAKE_FILE_IN} 0038 # Force rebuild every time, because we don't know the paths of the destination files 0039 # as they depend on the code. 0040 javac.depends = FORCE 0041 diff --git a/src/android/jar/AndroidManifest.xml b/src/android/jar/AndroidManifest.xml 0042 index cef88f7f19..ebc6fcfea7 100644 0043 --- a/src/android/jar/AndroidManifest.xml 0044 +++ b/src/android/jar/AndroidManifest.xml 0045 @@ -1,5 +1,4 @@ 0046 <?xml version='1.0' encoding='utf-8'?> 0047 <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="org.qtproject.qt5.android"> 0048 <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/> 0049 - <uses-sdk android:minSdkVersion="9" /> 0050 </manifest> 0051 diff --git a/src/android/jar/jar.pro b/src/android/jar/jar.pro 0052 index bda15a0a00..6a610c511d 100644 0053 --- a/src/android/jar/jar.pro 0054 +++ b/src/android/jar/jar.pro 0055 @@ -16,6 +16,9 @@ JAVASOURCES += \ 0056 $$PATHPREFIX/QtLayout.java \ 0057 $$PATHPREFIX/QtMessageDialogHelper.java \ 0058 $$PATHPREFIX/QtNative.java \ 0059 + $$PATHPREFIX/SAFFileManager.java \ 0060 + $$PATHPREFIX/SAFFile.java \ 0061 + $$PATHPREFIX/CachedDocumentFile.java \ 0062 $$PATHPREFIX/QtNativeLibrariesDir.java \ 0063 $$PATHPREFIX/QtSurface.java \ 0064 $$PATHPREFIX/ExtractStyle.java \ 0065 diff --git a/src/android/jar/src/org/qtproject/qt5/android/CachedDocumentFile.java b/src/android/jar/src/org/qtproject/qt5/android/CachedDocumentFile.java 0066 new file mode 100644 0067 index 0000000000..15dc727125 0068 --- /dev/null 0069 +++ b/src/android/jar/src/org/qtproject/qt5/android/CachedDocumentFile.java 0070 @@ -0,0 +1,225 @@ 0071 +package org.qtproject.qt5.android; 0072 + 0073 +import android.content.ContentResolver; 0074 +import android.content.Context; 0075 +import android.database.Cursor; 0076 +import android.net.Uri; 0077 +import android.os.Build; 0078 +import android.provider.DocumentsContract; 0079 +import android.text.TextUtils; 0080 +import android.util.Log; 0081 + 0082 +public class CachedDocumentFile { 0083 + 0084 + private static final String TAG = "CachedDocumentFile"; 0085 + private String name; 0086 + private final String mimeType; 0087 + private final String documentId; 0088 + // TODO(sh_zam): do something 0089 + private Integer size; 0090 + private Uri uri; 0091 + private final Context ctx; 0092 + private Boolean exists = null; 0093 + private Boolean writable = null; 0094 + 0095 + public CachedDocumentFile(Context context, String name, String documentId, String mimeType, Integer size, Uri uri) { 0096 + this.name = name; 0097 + this.documentId = documentId; 0098 + this.mimeType = mimeType; 0099 + this.size = size; 0100 + this.uri = uri; 0101 + this.ctx = context; 0102 + } 0103 + 0104 + public CachedDocumentFile(Context context, String name, String documentId, String mimeType, Uri uri) { 0105 + this(context, name, documentId, mimeType, -1, uri); 0106 + } 0107 + 0108 + public static CachedDocumentFile fromFileUri(Context context, Uri uri) { 0109 + final String[] columns = new String[]{ 0110 + DocumentsContract.Document.COLUMN_DISPLAY_NAME, 0111 + DocumentsContract.Document.COLUMN_DOCUMENT_ID, 0112 + DocumentsContract.Document.COLUMN_MIME_TYPE, 0113 + DocumentsContract.Document.COLUMN_SIZE, 0114 + }; 0115 + 0116 + Cursor cursor = null; 0117 + try { 0118 + final ContentResolver resolver = context.getContentResolver(); 0119 + cursor = resolver.query(uri, columns, null, null, null); 0120 + 0121 + if (cursor.moveToFirst()) { 0122 + return new CachedDocumentFile(context, 0123 + SAFUtils.getColumnValStringOrNull(cursor, DocumentsContract.Document.COLUMN_DISPLAY_NAME), 0124 + SAFUtils.getColumnValStringOrNull(cursor, DocumentsContract.Document.COLUMN_DOCUMENT_ID), 0125 + SAFUtils.getColumnValStringOrNull(cursor, DocumentsContract.Document.COLUMN_MIME_TYPE), 0126 + SAFUtils.getColumnValIntegerOrDefault(cursor, DocumentsContract.Document.COLUMN_SIZE, -1), 0127 + uri); 0128 + } 0129 + } catch (Exception e) { 0130 + Log.e(TAG, "fromFileUri(): " + e); 0131 + } finally { 0132 + if (cursor != null) 0133 + cursor.close(); 0134 + } 0135 + return null; 0136 + } 0137 + 0138 + public String getName() { 0139 + return name; 0140 + } 0141 + 0142 + public String getMimeType() { 0143 + return mimeType; 0144 + } 0145 + 0146 + public String getDocumentId() { 0147 + return documentId; 0148 + } 0149 + 0150 + /** 0151 + * @return document uri 0152 + */ 0153 + public Uri getUri() { 0154 + return uri; 0155 + } 0156 + 0157 + public boolean isFile() { 0158 + return !isDirectory() && !TextUtils.isEmpty(mimeType); 0159 + } 0160 + 0161 + public boolean isDirectory() { 0162 + return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType); 0163 + } 0164 + 0165 + public long getSize() { 0166 + return queryForLong(DocumentsContract.Document.COLUMN_SIZE, 0); 0167 + } 0168 + 0169 + public boolean rename(String displayName) { 0170 + try { 0171 + final Uri newUri = DocumentsContract.renameDocument( 0172 + ctx.getContentResolver(), uri, displayName); 0173 + if (newUri == null || newUri == uri) { 0174 + return false; 0175 + } 0176 + this.name = displayName; 0177 + this.uri = newUri; 0178 + return true; 0179 + } catch (Exception e) { 0180 + // HACK: see https://crbug.com/1246925. 0181 + if (isArc()) { 0182 + String oldUriStr = uri.toString(); 0183 + this.uri = Uri.parse(oldUriStr.replaceFirst(this.name + "$", displayName)); 0184 + this.exists = null; 0185 + if (exists()) { 0186 + this.name = displayName; 0187 + return true; 0188 + } else { 0189 + this.uri = Uri.parse(oldUriStr); 0190 + return false; 0191 + } 0192 + } 0193 + Log.e(TAG, "rename(): Rename failed: " + e); 0194 + return false; 0195 + } 0196 + } 0197 + 0198 + public boolean canWrite() { 0199 + if (writable != null) { 0200 + return writable; 0201 + } 0202 + writable = false; 0203 + Cursor cursor = null; 0204 + try { 0205 + final ContentResolver resolver = ctx.getContentResolver(); 0206 + final String[] columns = {DocumentsContract.Document.COLUMN_FLAGS, 0207 + DocumentsContract.Document.COLUMN_MIME_TYPE}; 0208 + cursor = resolver.query(uri, columns, null, null, null); 0209 + int flags = 0; 0210 + String mimeType = null; 0211 + if (cursor != null) { 0212 + if (cursor.moveToFirst()) { 0213 + flags = SAFUtils.getColumnValIntegerOrDefault(cursor, 0214 + DocumentsContract.Document.COLUMN_FLAGS, 0); 0215 + mimeType = SAFUtils.getColumnValStringOrNull(cursor, 0216 + DocumentsContract.Document.COLUMN_MIME_TYPE); 0217 + } 0218 + } 0219 + 0220 + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType) && 0221 + (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { 0222 + writable = true; 0223 + } else if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) { 0224 + writable = true; 0225 + } 0226 + 0227 + } catch (Exception e) { 0228 + Log.e(TAG, "canWrite(): Failed query: " + e); 0229 + writable = false; 0230 + } finally { 0231 + if (cursor != null) { 0232 + cursor.close(); 0233 + } 0234 + } 0235 + return writable; 0236 + } 0237 + 0238 + public boolean exists() { 0239 + if (exists != null) { 0240 + return exists; 0241 + } 0242 + 0243 + Cursor cursor = null; 0244 + try { 0245 + final ContentResolver resolver = ctx.getContentResolver(); 0246 + final String[] columns = {DocumentsContract.Document.COLUMN_DOCUMENT_ID}; 0247 + cursor = resolver.query(uri, columns, null, null, null); 0248 + exists = cursor.getCount() > 0; 0249 + } catch (Exception e) { 0250 + Log.e(TAG, "exists(): Failed query: " + e); 0251 + exists = false; 0252 + } finally { 0253 + if (cursor != null) { 0254 + cursor.close(); 0255 + } 0256 + } 0257 + 0258 + return exists; 0259 + } 0260 + 0261 + private long queryForLong(String column, long defaultValue) { 0262 + Cursor cursor = null; 0263 + try { 0264 + final ContentResolver resolver = ctx.getContentResolver(); 0265 + final String[] columns = {column}; 0266 + cursor = resolver.query(uri, columns, null, null, null); 0267 + if (cursor.moveToFirst() && !cursor.isNull(0)) { 0268 + return cursor.getLong(0); 0269 + } else { 0270 + return defaultValue; 0271 + } 0272 + } catch (Exception e) { 0273 + Log.e(TAG, "queryForLong(): Failed query: " + e); 0274 + return defaultValue; 0275 + } finally { 0276 + if (cursor != null) { 0277 + cursor.close(); 0278 + } 0279 + } 0280 + } 0281 + 0282 + @Override 0283 + public boolean equals(Object other) { 0284 + if (other instanceof CachedDocumentFile) { 0285 + return ((CachedDocumentFile) other).getUri().equals(this.getUri()); 0286 + } 0287 + return false; 0288 + } 0289 + 0290 + // we need some workarounds on ChromeOS 0291 + public static boolean isArc() { 0292 + return (Build.DEVICE != null) && Build.DEVICE.matches(".+_cheets|cheets_.+"); 0293 + } 0294 +} 0295 + 0296 diff --git a/src/android/jar/src/org/qtproject/qt5/android/QtNative.java b/src/android/jar/src/org/qtproject/qt5/android/QtNative.java 0297 index 2981523ab1..7e68206e1a 100644 0298 --- a/src/android/jar/src/org/qtproject/qt5/android/QtNative.java 0299 +++ b/src/android/jar/src/org/qtproject/qt5/android/QtNative.java 0300 @@ -40,22 +40,15 @@ 0301 0302 package org.qtproject.qt5.android; 0303 0304 -import java.io.File; 0305 -import java.io.FileNotFoundException; 0306 -import java.util.ArrayList; 0307 -import java.util.HashMap; 0308 -import java.util.concurrent.Semaphore; 0309 -import java.io.IOException; 0310 - 0311 import android.app.Activity; 0312 import android.app.Service; 0313 -import android.content.ActivityNotFoundException; 0314 -import android.content.Context; 0315 +import android.content.ClipData; 0316 +import android.content.ClipboardManager; 0317 import android.content.ContentResolver; 0318 +import android.content.Context; 0319 import android.content.Intent; 0320 -import android.content.pm.PackageManager; 0321 import android.content.pm.ActivityInfo; 0322 -import android.content.UriPermission; 0323 +import android.content.pm.PackageManager; 0324 import android.net.Uri; 0325 import android.os.Build; 0326 import android.os.Handler; 0327 @@ -65,26 +58,27 @@ import android.content.ClipboardManager; 0328 import android.content.ClipboardManager.OnPrimaryClipChangedListener; 0329 import android.content.ClipData; 0330 import android.content.ClipDescription; 0331 -import android.os.ParcelFileDescriptor; 0332 import android.util.Log; 0333 import android.view.ContextMenu; 0334 +import android.view.InputDevice; 0335 import android.view.KeyEvent; 0336 import android.view.Menu; 0337 import android.view.MotionEvent; 0338 import android.view.View; 0339 -import android.view.InputDevice; 0340 -import android.database.Cursor; 0341 -import android.provider.OpenableColumns; 0342 0343 +import java.io.File; 0344 import java.lang.reflect.Method; 0345 import java.security.KeyStore; 0346 import java.security.cert.X509Certificate; 0347 +import java.util.ArrayList; 0348 import java.util.Iterator; 0349 -import java.util.List; 0350 -import javax.net.ssl.TrustManagerFactory; 0351 +import java.util.concurrent.Semaphore; 0352 + 0353 import javax.net.ssl.TrustManager; 0354 +import javax.net.ssl.TrustManagerFactory; 0355 import javax.net.ssl.X509TrustManager; 0356 0357 +@SuppressWarnings("unused") 0358 public class QtNative 0359 { 0360 private static Activity m_activity = null; 0361 @@ -114,9 +108,6 @@ public class QtNative 0362 public static QtThread m_qtThread = new QtThread(); 0363 private static Method m_addItemMethod = null; 0364 0365 - private static HashMap<Integer, ParcelFileDescriptor> m_parcelFileDescriptors = new HashMap<Integer, ParcelFileDescriptor>(); 0366 - private static HashMap<Uri, Integer> m_uriPermissions = new HashMap<Uri, Integer>(); // for URIs which were not accessed through SAF e.g through an Intent 0367 - 0368 0369 private static final Runnable runPendingCppRunnablesRunnable = new Runnable() { 0370 @Override 0371 @@ -170,228 +161,6 @@ public class QtNative 0372 return joinedString.split(","); 0373 } 0374 0375 - public static void addToKnownUri(Uri uri, int modeFlags) { 0376 - m_uriPermissions.put(uri, modeFlags); 0377 - } 0378 - 0379 - public static boolean checkKnownUriPermission(Uri uri, String openMode) { 0380 - if (!m_uriPermissions.containsKey(uri)) { 0381 - return false; 0382 - } 0383 - 0384 - int modeFlags = 0; 0385 - if (openMode.startsWith("r")) { 0386 - modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; 0387 - } 0388 - 0389 - if (!"r".equals(openMode)) { 0390 - modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 0391 - } 0392 - 0393 - return (m_uriPermissions.get(uri) & modeFlags) == modeFlags; 0394 - } 0395 - 0396 - private static Uri getUriWithValidPermission(Context context, String uri, String openMode) 0397 - { 0398 - try { 0399 - List<UriPermission> permissions = context.getContentResolver().getPersistedUriPermissions(); 0400 - String uriStr = Uri.parse(uri).getPath(); 0401 - 0402 - for (int i = 0; i < permissions.size(); ++i) { 0403 - Uri iterUri = permissions.get(i).getUri(); 0404 - boolean isRightPermission = permissions.get(i).isReadPermission(); 0405 - 0406 - if (!openMode.equals("r")) 0407 - isRightPermission = permissions.get(i).isWritePermission(); 0408 - 0409 - if (iterUri.getPath().equals(uriStr) && isRightPermission) { 0410 - return iterUri; 0411 - } 0412 - } 0413 - 0414 - Uri uriParsed = Uri.parse(uri); 0415 - 0416 - // give known URIs a try, perhaps we got it in a way we couldn't persist the permissions (say Intent) 0417 - if (checkKnownUriPermission(uriParsed, openMode)) { 0418 - return uriParsed; 0419 - } 0420 - 0421 - return null; 0422 - } catch (SecurityException e) { 0423 - e.printStackTrace(); 0424 - return null; 0425 - } 0426 - } 0427 - 0428 - public static boolean openURL(Context context, String url, String mime) 0429 - { 0430 - Uri uri; 0431 - if (url.startsWith("content:")) { 0432 - uri = getUriWithValidPermission(context, url, "r"); 0433 - if (uri == null) { 0434 - Log.e(QtTAG, "openURL(): No permissions to open Uri"); 0435 - return false; 0436 - } 0437 - } else { 0438 - uri = Uri.parse(url); 0439 - } 0440 - 0441 - try { 0442 - Intent intent = new Intent(Intent.ACTION_VIEW, uri); 0443 - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 0444 - if (!mime.isEmpty()) 0445 - intent.setDataAndType(uri, mime); 0446 - 0447 - activity().startActivity(intent); 0448 - 0449 - return true; 0450 - } catch (IllegalArgumentException e) { 0451 - Log.e(QtTAG, "openURL(): Invalid Uri"); 0452 - return false; 0453 - } catch (UnsupportedOperationException e) { 0454 - Log.e(QtTAG, "openURL(): Unsupported operation for given Uri"); 0455 - return false; 0456 - } catch (ActivityNotFoundException e) { 0457 - e.printStackTrace(); 0458 - return false; 0459 - } 0460 - } 0461 - 0462 - public static int openFdForContentUrl(Context context, String contentUrl, String openMode) 0463 - { 0464 - Uri uri = getUriWithValidPermission(context, contentUrl, openMode); 0465 - int error = -1; 0466 - 0467 - if (uri == null) { 0468 - Log.e(QtTAG, "openFdForContentUrl(): No permissions to open Uri"); 0469 - return error; 0470 - } 0471 - 0472 - try { 0473 - ContentResolver resolver = context.getContentResolver(); 0474 - ParcelFileDescriptor fdDesc = resolver.openFileDescriptor(uri, openMode); 0475 - m_parcelFileDescriptors.put(fdDesc.getFd(), fdDesc); 0476 - return fdDesc.getFd(); 0477 - } catch (FileNotFoundException e) { 0478 - e.printStackTrace(); 0479 - return error; 0480 - } catch (IllegalArgumentException e) { 0481 - Log.e(QtTAG, "openFdForContentUrl(): Invalid Uri: " + e); 0482 - return error; 0483 - } 0484 - } 0485 - 0486 - public static boolean closeFd(int fd) 0487 - { 0488 - ParcelFileDescriptor pfd = m_parcelFileDescriptors.get(fd); 0489 - if (pfd == null) { 0490 - Log.wtf(QtTAG, "File descriptor doesn't exist in cache"); 0491 - return false; 0492 - } 0493 - 0494 - try { 0495 - pfd.close(); 0496 - return true; 0497 - } catch (IOException e) { 0498 - Log.e(QtTAG, "closeFd(): Failed to close the FD", e); 0499 - return false; 0500 - } 0501 - } 0502 - 0503 - public static long getSize(Context context, String contentUrl) 0504 - { 0505 - Uri uri = getUriWithValidPermission(context, contentUrl, "r"); 0506 - long size = -1; 0507 - 0508 - if (uri == null) { 0509 - Log.e(QtTAG, "getSize(): No permissions to open Uri"); 0510 - return size; 0511 - } 0512 - 0513 - try { 0514 - ContentResolver resolver = context.getContentResolver(); 0515 - Cursor cur = resolver.query(uri, null, null, null, null); 0516 - 0517 - if (cur != null) { 0518 - if (cur.moveToFirst()) 0519 - size = cur.getLong(cur.getColumnIndex(OpenableColumns.SIZE)); // size column 0520 - cur.close(); 0521 - } 0522 - return size; 0523 - } catch (IllegalArgumentException e) { 0524 - Log.e(QtTAG, "getSize(): Invalid Uri"); 0525 - return size; 0526 - } catch (UnsupportedOperationException e) { 0527 - Log.e(QtTAG, "getSize(): Unsupported operation for given Uri"); 0528 - return size; 0529 - } 0530 - } 0531 - 0532 - public static boolean checkFileExists(Context context, String contentUrl) 0533 - { 0534 - Uri uri = getUriWithValidPermission(context, contentUrl, "r"); 0535 - boolean exists = false; 0536 - 0537 - if (uri == null) { 0538 - Log.e(QtTAG, "checkFileExists(): No permissions to open Uri"); 0539 - return exists; 0540 - } 0541 - 0542 - try { 0543 - ContentResolver resolver = context.getContentResolver(); 0544 - Cursor cur = resolver.query(uri, null, null, null, null); 0545 - if (cur != null) { 0546 - exists = true; 0547 - cur.close(); 0548 - } 0549 - return exists; 0550 - } catch (IllegalArgumentException e) { 0551 - Log.e(QtTAG, "checkFileExists(): Invalid Uri"); 0552 - return exists; 0553 - } catch (UnsupportedOperationException e) { 0554 - Log.e(QtTAG, "checkFileExists(): Unsupported operation for given Uri"); 0555 - return false; 0556 - } 0557 - } 0558 - 0559 - public static boolean canWriteToUri(Context context, String contentUrl) 0560 - { 0561 - Uri uri = getUriWithValidPermission(context, contentUrl, "w"); 0562 - 0563 - if (uri == null) { 0564 - Log.e(QtTAG, "canWriteToUri(): No permissions to open Uri in \"w\" mode"); 0565 - return false; 0566 - } else { 0567 - return true; 0568 - } 0569 - } 0570 - 0571 - public static String getFileNameFromUri(Context context, String contentUrl) 0572 - { 0573 - Uri uri = getUriWithValidPermission(context, contentUrl, "r"); 0574 - if (uri == null) { 0575 - Log.e(QtTAG, "getFileNameFromUri(): No permissions to open Uri:" + contentUrl); 0576 - return null; 0577 - } 0578 - 0579 - String filename = null; 0580 - try { 0581 - Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); 0582 - if (cursor != null) { 0583 - if (cursor.moveToFirst()) { 0584 - filename = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); 0585 - } 0586 - cursor.close(); 0587 - } 0588 - } catch (IllegalArgumentException e) { 0589 - Log.e(QtTAG, "getFileNameFromUri(): Couldn't get filename: " + e.getMessage()); 0590 - } catch (UnsupportedOperationException e) { 0591 - Log.e(QtTAG, "getFileNameFromUri(): UnsupportedOperation on the Uri: " + e.getMessage()); 0592 - } 0593 - 0594 - return filename; 0595 - } 0596 - 0597 // this method loads full path libs 0598 public static void loadQtLibraries(final ArrayList<String> libraries) 0599 { 0600 diff --git a/src/android/jar/src/org/qtproject/qt5/android/SAFFile.java b/src/android/jar/src/org/qtproject/qt5/android/SAFFile.java 0601 new file mode 100644 0602 index 0000000000..fd371bebc2 0603 --- /dev/null 0604 +++ b/src/android/jar/src/org/qtproject/qt5/android/SAFFile.java 0605 @@ -0,0 +1,23 @@ 0606 +package org.qtproject.qt5.android; 0607 + 0608 +import android.net.Uri; 0609 + 0610 +import java.util.List; 0611 + 0612 +public class SAFFile { 0613 + private Uri baseUri; 0614 + private List<String> segments; 0615 + 0616 + SAFFile(Uri baseUri, List<String> segments) { 0617 + this.baseUri = baseUri; 0618 + this.segments = segments; 0619 + } 0620 + 0621 + List<String> getSegments() { 0622 + return segments; 0623 + } 0624 + 0625 + public Uri getBaseUri() { 0626 + return baseUri; 0627 + } 0628 +} 0629 diff --git a/src/android/jar/src/org/qtproject/qt5/android/SAFFileManager.java b/src/android/jar/src/org/qtproject/qt5/android/SAFFileManager.java 0630 new file mode 100644 0631 index 0000000000..5191bfb0d4 0632 --- /dev/null 0633 +++ b/src/android/jar/src/org/qtproject/qt5/android/SAFFileManager.java 0634 @@ -0,0 +1,800 @@ 0635 +package org.qtproject.qt5.android; 0636 + 0637 +import android.annotation.SuppressLint; 0638 +import android.content.ActivityNotFoundException; 0639 +import android.content.ContentResolver; 0640 +import android.content.Context; 0641 +import android.content.Intent; 0642 +import android.content.UriPermission; 0643 +import android.content.pm.PackageManager; 0644 +import android.database.Cursor; 0645 +import android.net.Uri; 0646 +import android.os.Build; 0647 +import android.os.ParcelFileDescriptor; 0648 +import android.provider.DocumentsContract; 0649 +import android.util.Log; 0650 +import android.webkit.MimeTypeMap; 0651 + 0652 +import java.io.File; 0653 +import java.io.IOException; 0654 +import java.util.ArrayList; 0655 +import java.util.HashMap; 0656 +import java.util.Iterator; 0657 +import java.util.List; 0658 +import java.util.Map; 0659 + 0660 +@SuppressWarnings("unused") 0661 +class FileError { 0662 + public static final String TAG = "SAFFileManager.FileError"; 0663 + public static final int NO_ERROR = 0; 0664 + public static final int READ_ERROR = 1; 0665 + public static final int WRITE_ERROR = 2; 0666 + public static final int FATAL_ERROR = 3; 0667 + public static final int RESOURCE_ERROR = 4; 0668 + public static final int OPEN_ERROR = 5; 0669 + public static final int ABORT_ERROR = 6; 0670 + public static final int TIME_OUT_ERROR = 7; 0671 + public static final int UNSPECIFIED_ERROR = 8; 0672 + public static final int REMOVE_ERROR = 9; 0673 + public static final int RENAME_ERROR = 10; 0674 + public static final int POSITION_ERROR = 11; 0675 + public static final int RESIZE_ERROR = 12; 0676 + public static final int PERMISSIONS_ERROR = 13; 0677 + public static final int COPY_ERROR = 14; 0678 + 0679 + private String errorString; 0680 + private int error; 0681 + 0682 + public String getErrorString() { 0683 + return errorString; 0684 + } 0685 + 0686 + public void setErrorString(String errorString) { 0687 + this.errorString = errorString; 0688 + if (error != FileError.NO_ERROR) { 0689 + Log.w(TAG, errorString); 0690 + } 0691 + } 0692 + 0693 + public int getError() { 0694 + return error; 0695 + } 0696 + 0697 + public void setError(int error) { 0698 + this.error = error; 0699 + } 0700 + 0701 + public void setUnknownError() { 0702 + setError(FileError.UNSPECIFIED_ERROR); 0703 + setErrorString("Unknown Error"); 0704 + } 0705 + 0706 + public void unsetError() { 0707 + setError(NO_ERROR); 0708 + setErrorString("No error"); 0709 + } 0710 +} 0711 + 0712 +// Native usage 0713 +@SuppressWarnings("UnusedDeclaration") 0714 +public class SAFFileManager { 0715 + 0716 + private static final String TAG = "SAFFileManager"; 0717 + private static final String PATH_TREE = "tree"; 0718 + 0719 + @SuppressLint("StaticFieldLeak") // TODO(sh_zam): we only have one activity! 0720 + private static SAFFileManager sSafFileManager; 0721 + 0722 + private final Context mCtx; 0723 + private final HashMap<Uri, CachedDocumentFile> mCachedDocumentFiles = new HashMap<>(); 0724 + private final HashMap<Integer, ParcelFileDescriptor> m_parcelFileDescriptors = new HashMap<>(); 0725 + 0726 + private final FileError mError = new FileError(); 0727 + private List<UriPermission> mCachedPermissions; 0728 + private final ArrayList<Uri> mCachedListDocumentFiles = new ArrayList<>(); 0729 + 0730 + SAFFileManager(Context ctx) { 0731 + mCtx = ctx; 0732 + } 0733 + 0734 + // Native usage 0735 + @SuppressWarnings("UnusedDeclaration") 0736 + public static SAFFileManager instance() { 0737 + if (sSafFileManager == null) { 0738 + sSafFileManager = new SAFFileManager(QtNative.getContext()); 0739 + } 0740 + return sSafFileManager; 0741 + } 0742 + 0743 + public static SAFFileManager instance(Context context) { 0744 + if (sSafFileManager == null) { 0745 + sSafFileManager = new SAFFileManager(context); 0746 + } 0747 + return sSafFileManager; 0748 + } 0749 + 0750 + private boolean checkImplicitUriPermission(Uri uri, String openMode) { 0751 + int modeFlags = 0; 0752 + if (openMode.startsWith("r")) { 0753 + modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; 0754 + } 0755 + 0756 + if (!"r".equals(openMode)) { 0757 + modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 0758 + } 0759 + 0760 + return mCtx.checkCallingOrSelfUriPermission(uri, modeFlags) == 0761 + PackageManager.PERMISSION_GRANTED; 0762 + } 0763 + 0764 + void resetCachedPermission() { 0765 + mCachedPermissions = mCtx.getContentResolver().getPersistedUriPermissions(); 0766 + } 0767 + 0768 + /** 0769 + * The encoding of Path segments is somewhat arbitrary and something which doesn't 0770 + * seem very safe to rely on. So if we find a matching path we just return 0771 + * the persisted Uri.<br> 0772 + * <p> 0773 + * This function also handles the permission check of implicitly sent Uris. 0774 + * 0775 + * @param uri content Uri, can be Tree Uri or a Single file Uri 0776 + * @param openMode mode to open the file in 0777 + * @return The persisted Uri opened through ACTION_OPEN_DOCUMENT, 0778 + * ACTION_OPEN_DOCUMENT_TREE i.e only if we have permission to the Uri. 0779 + */ 0780 + Uri getProperlyEncodedUriWithPermissions(Uri uri, String openMode) { 0781 + if (mCachedPermissions == null) { 0782 + resetCachedPermission(); 0783 + } 0784 + final String uriPath = uri.getPath(); 0785 + 0786 + for (int i = 0; i < mCachedPermissions.size(); ++i) { 0787 + Uri iterUri = mCachedPermissions.get(i).getUri(); 0788 + boolean isRightPermission = mCachedPermissions.get(i).isReadPermission(); 0789 + 0790 + if (!openMode.equals("r")) 0791 + isRightPermission = mCachedPermissions.get(i).isWritePermission(); 0792 + 0793 + if (iterUri.getPath().equals(uriPath) && isRightPermission) { 0794 + return iterUri; 0795 + } 0796 + } 0797 + 0798 + // TODO(sh_zam): check encoding 0799 + // TODO(sh_zam): verify if intent has to exist 0800 + // check if we received permission from an Intent 0801 + return (QtNative.activity() != null && 0802 + QtNative.activity().getIntent() != null && 0803 + checkImplicitUriPermission(uri, openMode)) ? uri : null; 0804 + } 0805 + 0806 + /** 0807 + * Check if we have permission to the Uri. This method also handles tree Uris. 0808 + * If a Tree Uri is passed in {@code openMode} including write permissions 0809 + * we create the file under the tree if we have permissions to it.<br> 0810 + * <p> 0811 + * Note: The results are undefined for Uris of all types other than: 0812 + * {@code tree/*} and {@code document/*}. 0813 + * 0814 + * @param url content Uri, can be Tree Uri or a Single file Uri 0815 + * @param openMode mode to open the file in 0816 + * @return Uri of the newly created file or passed in url if we have 0817 + * permission to access the document, otherwise null. 0818 + */ 0819 + private CachedDocumentFile getDocumentFileWithValidPermissions(String url, 0820 + String openMode, 0821 + boolean dontCreateDoc) { 0822 + final Uri uri = Uri.parse(url); 0823 + 0824 + // it is a file in tree, so we create a new file if "w" 0825 + if (isTreeUri(uri)) { 0826 + SAFFile rawSafFile = nearestTreeUri(uri); 0827 + if (rawSafFile == null) { 0828 + mError.setError(FileError.PERMISSIONS_ERROR); 0829 + mError.setErrorString("No permission to access the Document Tree"); 0830 + return null; 0831 + } 0832 + 0833 + CachedDocumentFile foundFile = findFileInTree(rawSafFile); 0834 + 0835 + if (foundFile != null) { 0836 + return foundFile; 0837 + } 0838 + 0839 + // we shouldn't create a file here 0840 + if ("r".equals(openMode) || dontCreateDoc) { 0841 + return null; 0842 + } 0843 + 0844 + return createFile(rawSafFile, false); 0845 + } else { 0846 + Uri resultUri = getProperlyEncodedUriWithPermissions(uri, openMode); 0847 + if (resultUri != null) { 0848 + return CachedDocumentFile.fromFileUri(mCtx, resultUri); 0849 + } 0850 + } 0851 + 0852 + mError.setError(FileError.PERMISSIONS_ERROR); 0853 + mError.setErrorString("No permission to access the Uri"); 0854 + return null; 0855 + } 0856 + 0857 + private CachedDocumentFile getDocumentFileWithValidPermissions(String url, 0858 + String openMode) { 0859 + return getDocumentFileWithValidPermissions(url, openMode, false); 0860 + } 0861 + 0862 + // Native usage 0863 + @SuppressWarnings("UnusedDeclaration") 0864 + private boolean launchUri(String url, String mime) { 0865 + Uri uri; 0866 + if (url.startsWith("content:")) { 0867 + uri = getDocumentFileWithValidPermissions(url, "r").getUri(); 0868 + if (uri == null) { 0869 + Log.e(TAG, "launchUri(): No permissions to open Uri"); 0870 + return false; 0871 + } 0872 + } else { 0873 + uri = Uri.parse(url); 0874 + } 0875 + 0876 + try { 0877 + Intent intent = new Intent(Intent.ACTION_VIEW, uri); 0878 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 0879 + if (!mime.isEmpty()) 0880 + intent.setDataAndType(uri, mime); 0881 + 0882 + QtNative.activity().startActivity(intent); 0883 + 0884 + return true; 0885 + } catch (IllegalArgumentException e) { 0886 + Log.e(TAG, "launchUri(): Invalid Uri"); 0887 + return false; 0888 + } catch (UnsupportedOperationException e) { 0889 + Log.e(TAG, "launchUri(): Unsupported operation for given Uri"); 0890 + return false; 0891 + } catch (ActivityNotFoundException e) { 0892 + e.printStackTrace(); 0893 + return false; 0894 + } 0895 + } 0896 + 0897 + // Native usage 0898 + @SuppressWarnings("UnusedDeclaration") 0899 + synchronized public int openFileDescriptor(String contentUrl, String openMode) { 0900 + int retry = 0; 0901 + while (retry < 2) { 0902 + CachedDocumentFile file = 0903 + getDocumentFileWithValidPermissions(contentUrl, openMode); 0904 + 0905 + if (file == null) { 0906 + return -1; 0907 + } 0908 + 0909 + // take this out 0910 + try { 0911 + ContentResolver resolver = mCtx.getContentResolver(); 0912 + ParcelFileDescriptor fdDesc = 0913 + resolver.openFileDescriptor(file.getUri(), openMode); 0914 + m_parcelFileDescriptors.put(fdDesc.getFd(), fdDesc); 0915 + 0916 + mError.unsetError(); 0917 + return fdDesc.getFd(); 0918 + } catch (Exception e) { 0919 + Log.w(TAG, "openFileDescriptor(): Failed query: " + e); 0920 + mCachedDocumentFiles.remove(file.getUri()); 0921 + retry++; 0922 + } 0923 + } 0924 + 0925 + mError.setError(FileError.WRITE_ERROR); 0926 + mError.setErrorString("Couldn't open file for writing"); 0927 + return -1; 0928 + } 0929 + 0930 + // Native usage 0931 + @SuppressWarnings("UnusedDeclaration") 0932 + public boolean closeFileDescriptor(int fd) { 0933 + ParcelFileDescriptor pfd = m_parcelFileDescriptors.get(fd); 0934 + if (pfd == null) { 0935 + Log.wtf(TAG, "File descriptor doesn't exist in cache"); 0936 + return false; 0937 + } 0938 + 0939 + try { 0940 + mError.unsetError(); 0941 + pfd.close(); 0942 + return true; 0943 + } catch (IOException e) { 0944 + Log.e(TAG, "closeFileDescriptor(): Failed to close the FD", e); 0945 + } 0946 + mError.setUnknownError(); 0947 + return false; 0948 + } 0949 + 0950 + // Native usage 0951 + @SuppressWarnings("UnusedDeclaration") 0952 + public long getSize(String contentUrl) { 0953 + CachedDocumentFile file = 0954 + getDocumentFileWithValidPermissions(contentUrl, "r"); 0955 + 0956 + if (file != null) { 0957 + return file.getSize(); 0958 + } else { 0959 + mError.setUnknownError(); 0960 + return 0; 0961 + } 0962 + } 0963 + 0964 + // Native usage 0965 + @SuppressWarnings("UnusedDeclaration") 0966 + public boolean exists(String contentUrl) { 0967 + final CachedDocumentFile file = 0968 + getDocumentFileWithValidPermissions(contentUrl, "r"); 0969 + 0970 + if (file != null && file.exists()) { 0971 + mError.unsetError(); 0972 + return true; 0973 + } else { 0974 + mError.setUnknownError(); 0975 + return false; 0976 + } 0977 + } 0978 + 0979 + // Native usage 0980 + @SuppressWarnings("UnusedDeclaration") 0981 + public boolean canWrite(String contentUrl) { 0982 + final CachedDocumentFile file = 0983 + getDocumentFileWithValidPermissions(contentUrl, "w", true); 0984 + 0985 + if (file != null && file.canWrite()) { 0986 + mError.unsetError(); 0987 + return true; 0988 + } else { 0989 + return false; 0990 + } 0991 + } 0992 + 0993 + // Native usage 0994 + @SuppressWarnings("UnusedDeclaration") 0995 + public String getFileName(String contentUrl) { 0996 + final CachedDocumentFile file = 0997 + getDocumentFileWithValidPermissions(contentUrl, "r"); 0998 + 0999 + if (file != null) { 1000 + mError.unsetError(); 1001 + return file.getName(); 1002 + } 1003 + 1004 + return null; 1005 + } 1006 + 1007 + private String stringJoin(String delimiter, List<String> list) { 1008 + if (list.size() < 1) { 1009 + return ""; 1010 + } 1011 + final StringBuilder builder = new StringBuilder(); 1012 + for (int i = 0; i < list.size(); ++i) { 1013 + builder.append(list.get(i)) 1014 + .append(delimiter); 1015 + } 1016 + 1017 + // remove trailing 1018 + builder.delete(builder.length() - delimiter.length(), builder.length()); 1019 + return builder.toString(); 1020 + } 1021 + 1022 + private Uri uriAppend(Uri uri, List<String> items) { 1023 + final StringBuilder builder = new StringBuilder(uri.toString()); 1024 + for (String item : items) { 1025 + builder.append(Uri.encode(File.separator)).append(item); 1026 + } 1027 + return Uri.parse(builder.toString()); 1028 + } 1029 + 1030 + /** 1031 + * Splits the tree uri: 1032 + * <p> 1033 + * {@code "content://com.externalstorage.documents/tree/Primary%3AExample/path1/path2"} 1034 + * into: <br> 1035 + * First = {@code "content://com.externalstorage.documents/tree/Primary%3AExample"}, 1036 + * <br> 1037 + * Second = {@code "path1/path2"} 1038 + * 1039 + * @param treeUri The Tree Uri with a path appended - which may or may not exist 1040 + * @return a {@link SAFFile} through which we can get the base Uri and path segments 1041 + * which are to be created or tested 1042 + */ 1043 + private SAFFile nearestTreeUri(Uri treeUri) { 1044 + final List<String> paths = treeUri.getPathSegments() 1045 + .subList(1, treeUri.getPathSegments().size()); 1046 + 1047 + // Test each subtree, going from right to left 1048 + for (int i = paths.size(); i > 0; --i) { 1049 + final Uri baseUri = 1050 + new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 1051 + .authority(treeUri.getAuthority()).appendPath(PATH_TREE) 1052 + .appendPath(paths.get(0)).build(); 1053 + 1054 + // we can't use appendPath, because of the weird encoding rules that SAF follows 1055 + final Uri testUri = getProperlyEncodedUriWithPermissions( 1056 + uriAppend(baseUri, paths.subList(1, i)), "rw"); 1057 + 1058 + // we check the permission of the subtree 1059 + if (testUri != null) { 1060 + if (i < paths.size()) { 1061 + return new SAFFile(testUri, paths.subList(i, paths.size())); 1062 + } else { 1063 + return new SAFFile(testUri, new ArrayList<String>()); 1064 + } 1065 + } 1066 + } 1067 + 1068 + Log.d(TAG, "nearestTreeUri(): No permissions to Uri: " + treeUri); 1069 + return null; 1070 + } 1071 + 1072 + // Native usage 1073 + @SuppressWarnings("UnusedDeclaration") 1074 + synchronized public boolean delete(String contentUrl) { 1075 + final CachedDocumentFile file = 1076 + getDocumentFileWithValidPermissions(contentUrl, "rw", true); 1077 + if (file == null) { 1078 + return false; 1079 + } 1080 + 1081 + mCachedDocumentFiles.remove(file.getUri()); 1082 + if (file.isDirectory()) { 1083 + invalidateCachedDocuments(file.getUri()); 1084 + } 1085 + return deleteFile(file.getUri()); 1086 + } 1087 + 1088 + // Native usage 1089 + public String[] listFileNames(String contentUrl) { 1090 + final CachedDocumentFile file = 1091 + getDocumentFileWithValidPermissions(contentUrl, "r"); 1092 + 1093 + if (file == null || !file.isDirectory()) { 1094 + return null; 1095 + } 1096 + 1097 + List<CachedDocumentFile> files = listFiles(file.getUri()); 1098 + String[] result = new String[files.size()]; 1099 + for (int i = 0; i < files.size(); ++i) { 1100 + CachedDocumentFile docFile = files.get(i); 1101 + result[i] = docFile.getName(); 1102 + mCachedDocumentFiles.put(docFile.getUri(), docFile); 1103 + mCachedListDocumentFiles.add(docFile.getUri()); 1104 + } 1105 + 1106 + return result; 1107 + } 1108 + 1109 + // Native usage 1110 + void resetListCache() { 1111 + for (Uri uri : mCachedListDocumentFiles) { 1112 + mCachedDocumentFiles.remove(uri); 1113 + } 1114 + mCachedListDocumentFiles.clear(); 1115 + } 1116 + 1117 + /** 1118 + * A dumb heuristic method to remove the subdirectories if the parent directory 1119 + * is removed. 1120 + * 1121 + * @param removedUri The document to be deleted 1122 + */ 1123 + private void invalidateCachedDocuments(Uri removedUri) { 1124 + String dirname = removedUri.getLastPathSegment(); 1125 + Iterator<Map.Entry<Uri, CachedDocumentFile>> iterator = 1126 + mCachedDocumentFiles.entrySet().iterator(); 1127 + while (iterator.hasNext()) { 1128 + Map.Entry<Uri, CachedDocumentFile> entry = iterator.next(); 1129 + if (entry.getKey().getPath().contains(dirname)) { 1130 + iterator.remove(); 1131 + } 1132 + } 1133 + } 1134 + 1135 + // Native usage 1136 + @SuppressWarnings("UnusedDeclaration") 1137 + public boolean isDir(String contentUrl) { 1138 + final CachedDocumentFile file = 1139 + getDocumentFileWithValidPermissions(contentUrl, "rw", true); 1140 + if (file == null) { 1141 + return false; 1142 + } 1143 + 1144 + mError.unsetError(); 1145 + return file.isDirectory(); 1146 + } 1147 + 1148 + private boolean isTreeUri(Uri uri) { 1149 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 1150 + return DocumentsContract.isTreeUri(uri); 1151 + } else { 1152 + final List<String> paths = uri.getPathSegments(); 1153 + return (paths.size() >= 2 && PATH_TREE.equals(paths.get(0))); 1154 + } 1155 + } 1156 + 1157 + // Native usage 1158 + public boolean isTreeUri(String contentUrl) { 1159 + return isTreeUri(Uri.parse(contentUrl)); 1160 + } 1161 + 1162 + synchronized public boolean rename(String contentUrl, String displayName) { 1163 + final CachedDocumentFile file = 1164 + getDocumentFileWithValidPermissions(contentUrl, "rw", true); 1165 + if (file == null) { 1166 + return false; 1167 + } 1168 + 1169 + final Uri oldUri = file.getUri(); 1170 + if (file.rename(displayName)) { 1171 + mCachedDocumentFiles.remove(oldUri); 1172 + invalidateCachedDocuments(oldUri); 1173 + mCachedDocumentFiles.put(file.getUri(), file); 1174 + resetCachedPermission(); 1175 + return true; 1176 + } else { 1177 + return false; 1178 + } 1179 + } 1180 + 1181 + // Native usage 1182 + @SuppressWarnings("UnusedDeclaration") 1183 + synchronized public boolean mkdir(String contentUrl, boolean createParentDirectories) { 1184 + if (isDir(contentUrl)) { 1185 + return true; 1186 + } 1187 + 1188 + final Uri uri = Uri.parse(contentUrl); 1189 + // "tree" and document id make the first two parts of the path 1190 + if (uri.getPathSegments().size() > 3 && !createParentDirectories) { 1191 + return false; 1192 + } 1193 + final SAFFile rawSafFile = nearestTreeUri(uri); 1194 + if (rawSafFile == null) { 1195 + mError.setError(FileError.PERMISSIONS_ERROR); 1196 + mError.setErrorString("No permission to access the Document Tree"); 1197 + return false; 1198 + } 1199 + 1200 + if (createDirectories(rawSafFile) != null) { 1201 + mError.unsetError(); 1202 + return true; 1203 + } else { 1204 + return false; 1205 + } 1206 + } 1207 + 1208 + Uri createDirectories(SAFFile file) { 1209 + final Uri treeUri = file.getBaseUri(); 1210 + List<String> pathSegments = file.getSegments(); 1211 + 1212 + Log.d(TAG, "Creating directory: " + treeUri.toString() + 1213 + ", segments = " + stringJoin("/", pathSegments)); 1214 + 1215 + Uri parent = DocumentsContract.buildDocumentUriUsingTree(treeUri, 1216 + DocumentsContract.getTreeDocumentId(treeUri)); 1217 + for (String segment : pathSegments) { 1218 + final CachedDocumentFile existingFile = findFile(parent, segment); 1219 + 1220 + if (existingFile != null) { 1221 + Log.d(TAG, "Exists: " + existingFile.getUri().toString()); 1222 + if (existingFile.isFile()) { 1223 + mError.setError(FileError.UNSPECIFIED_ERROR); 1224 + mError.setErrorString( 1225 + "Couldn't create a directory at the specified path, because a file with same name exists"); 1226 + return null; 1227 + } 1228 + 1229 + parent = existingFile.getUri(); 1230 + continue; 1231 + } 1232 + 1233 + final CachedDocumentFile newFile = createDirectory(parent, segment); 1234 + if (newFile == null) { 1235 + return null; 1236 + } 1237 + parent = newFile.getUri(); 1238 + mCachedDocumentFiles.put(newFile.getUri(), newFile); 1239 + } 1240 + 1241 + mError.unsetError(); 1242 + return parent; 1243 + } 1244 + 1245 + synchronized private CachedDocumentFile createFile(SAFFile file, boolean force) { 1246 + 1247 + List<String> pathSegments = file.getSegments(); 1248 + 1249 + Log.d(TAG, "Creating new file: " + file.getBaseUri() + ", filename = " 1250 + + stringJoin(File.separator, pathSegments)); 1251 + 1252 + final Uri parent = createDirectories( 1253 + new SAFFile(file.getBaseUri(), 1254 + pathSegments.subList(0, pathSegments.size() - 1))); 1255 + if (parent == null) { 1256 + return null; 1257 + } 1258 + 1259 + final String filename = pathSegments.get(pathSegments.size() - 1); 1260 + final String mimeType = getMimeTypeFromFilename(filename); 1261 + 1262 + final CachedDocumentFile foundFile = findFile(parent, filename); 1263 + if (foundFile != null && foundFile.isFile() && !force) { 1264 + return foundFile; 1265 + } 1266 + 1267 + final CachedDocumentFile newFile = createDocumentImpl(parent, filename, mimeType); 1268 + if (newFile == null) { 1269 + return null; 1270 + } 1271 + mCachedDocumentFiles.put(newFile.getUri(), newFile); 1272 + return newFile; 1273 + } 1274 + 1275 + private String getMimeTypeFromFilename(String filename) { 1276 + final int index = filename.lastIndexOf("."); 1277 + String extension; 1278 + if (index == -1 || index == filename.length() - 1) { 1279 + extension = ""; 1280 + } else { 1281 + extension = filename.substring(index + 1); 1282 + } 1283 + 1284 + if (extension.isEmpty()) { 1285 + return "application/octet-stream"; 1286 + } 1287 + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 1288 + } 1289 + 1290 + 1291 + private List<CachedDocumentFile> listFiles(Uri documentTreeUri) { 1292 + Log.d(TAG, "listFiles(): Uri = " + documentTreeUri); 1293 + final List<CachedDocumentFile> cachedDocumentFiles = new ArrayList<>(); 1294 + // order matters! 1295 + final String[] columns = new String[]{ 1296 + DocumentsContract.Document.COLUMN_DISPLAY_NAME, 1297 + DocumentsContract.Document.COLUMN_DOCUMENT_ID, 1298 + DocumentsContract.Document.COLUMN_MIME_TYPE, 1299 + DocumentsContract.Document.COLUMN_SIZE 1300 + }; 1301 + 1302 + Cursor cursor = null; 1303 + try { 1304 + final ContentResolver resolver = mCtx.getContentResolver(); 1305 + final Uri childrenTreeUri = DocumentsContract.buildChildDocumentsUriUsingTree(documentTreeUri, 1306 + DocumentsContract.getDocumentId(documentTreeUri)); 1307 + cursor = resolver.query(childrenTreeUri, columns, null, null, null); 1308 + if (cursor == null) { 1309 + return cachedDocumentFiles; 1310 + } 1311 + 1312 + while (cursor.moveToNext()) { 1313 + final String docId = cursor.getString(1); 1314 + final Uri fileUri = DocumentsContract.buildDocumentUriUsingTree(documentTreeUri, docId); 1315 + cachedDocumentFiles.add(new CachedDocumentFile(mCtx, 1316 + SAFUtils.getColumnValStringOrNull(cursor, DocumentsContract.Document.COLUMN_DISPLAY_NAME), 1317 + SAFUtils.getColumnValStringOrNull(cursor, DocumentsContract.Document.COLUMN_DOCUMENT_ID), 1318 + SAFUtils.getColumnValStringOrNull(cursor, DocumentsContract.Document.COLUMN_MIME_TYPE), 1319 + SAFUtils.getColumnValIntegerOrDefault(cursor, DocumentsContract.Document.COLUMN_SIZE, -1), 1320 + fileUri)); 1321 + } 1322 + } catch (Exception e) { 1323 + Log.e(TAG, "Invalid document Uri: " + documentTreeUri); 1324 + // TODO(sh_zam): a test is needed 1325 + mCachedDocumentFiles.remove(documentTreeUri); 1326 + invalidateCachedDocuments(documentTreeUri); 1327 + } finally { 1328 + if (cursor != null) 1329 + cursor.close(); 1330 + } 1331 + 1332 + return cachedDocumentFiles; 1333 + } 1334 + 1335 + /** 1336 + * Find the file under the tree. This function can take pathSegments which 1337 + * allow looking recursively in the document's subtree. 1338 + * 1339 + * @param safFile {@link SAFFile} 1340 + * @return {@link CachedDocumentFile} if the document is found, null otherwise. 1341 + */ 1342 + private CachedDocumentFile findFileInTree(SAFFile safFile) { 1343 + List<String> pathSegments = safFile.getSegments(); 1344 + Uri parent = DocumentsContract.buildDocumentUriUsingTree(safFile.getBaseUri(), 1345 + DocumentsContract.getTreeDocumentId(safFile.getBaseUri())); 1346 + 1347 + CachedDocumentFile documentFile; 1348 + if (mCachedDocumentFiles.containsKey(parent)) { 1349 + documentFile = mCachedDocumentFiles.get(parent); 1350 + } else { 1351 + documentFile = CachedDocumentFile.fromFileUri(mCtx, parent); 1352 + mCachedDocumentFiles.put(documentFile.getUri(), documentFile); 1353 + } 1354 + 1355 + for (int i = 0; i < pathSegments.size(); ++i) { 1356 + documentFile = findFile(parent, pathSegments.get(i)); 1357 + if (documentFile == null) { 1358 + return null; 1359 + } 1360 + 1361 + if (documentFile.isFile()) { 1362 + if (i == pathSegments.size() - 1) { 1363 + return documentFile; 1364 + } else { 1365 + return null; 1366 + } 1367 + } 1368 + parent = documentFile.getUri(); 1369 + } 1370 + 1371 + return documentFile; 1372 + } 1373 + 1374 + /** 1375 + * Find the file under subtree 1376 + * 1377 + * @param documentTreeUri a Uri with both "tree" and "document". 1378 + * @param filename name of the file or directory. 1379 + * @return {@link CachedDocumentFile} if the document is found in cache or under 1380 + * the tree, null otherwise. 1381 + */ 1382 + private CachedDocumentFile findFile(Uri documentTreeUri, String filename) { 1383 + { 1384 + final Uri expectedUri = DocumentsContract.buildDocumentUriUsingTree(documentTreeUri, 1385 + DocumentsContract.getDocumentId(documentTreeUri) + "/" + filename); 1386 + // check in the cached documents first 1387 + if (mCachedDocumentFiles.containsKey(expectedUri)) { 1388 + return mCachedDocumentFiles.get(expectedUri); 1389 + } 1390 + } 1391 + 1392 + // check the tree now 1393 + List<CachedDocumentFile> cachedDocumentFiles = listFiles(documentTreeUri); 1394 + for (CachedDocumentFile file : cachedDocumentFiles) { 1395 + if (filename.equals(file.getName())) { 1396 + mCachedDocumentFiles.put(file.getUri(), file); 1397 + return file; 1398 + } 1399 + } 1400 + 1401 + return null; 1402 + } 1403 + 1404 + private CachedDocumentFile createDocumentImpl(Uri parent, String displayName, String mimeType) { 1405 + try { 1406 + final Uri fileUri = DocumentsContract.createDocument(mCtx.getContentResolver(), 1407 + parent, mimeType, displayName); 1408 + return new CachedDocumentFile(mCtx, displayName, 1409 + DocumentsContract.getDocumentId(fileUri), 1410 + mimeType, 1411 + fileUri); 1412 + } catch (Exception e) { 1413 + mError.setUnknownError(); 1414 + Log.e(TAG, "Error creating a file: uri = " + parent + 1415 + ", displayName = " + displayName + ", mimeType = " + mimeType); 1416 + return null; 1417 + } 1418 + } 1419 + 1420 + private boolean deleteFile(Uri documentUri) { 1421 + try { 1422 + return DocumentsContract.deleteDocument(mCtx.getContentResolver(), 1423 + documentUri); 1424 + } catch (Exception e) { 1425 + mError.setUnknownError(); 1426 + Log.e(TAG, "Error deleting a file: uri = " + documentUri); 1427 + return false; 1428 + } 1429 + } 1430 + 1431 + private CachedDocumentFile createDirectory(Uri parent, String displayName) { 1432 + return createDocumentImpl(parent, displayName, DocumentsContract.Document.MIME_TYPE_DIR); 1433 + } 1434 +} 1435 diff --git a/src/android/jar/src/org/qtproject/qt5/android/SAFUtils.java b/src/android/jar/src/org/qtproject/qt5/android/SAFUtils.java 1436 new file mode 100644 1437 index 0000000000..51f8f710df 1438 --- /dev/null 1439 +++ b/src/android/jar/src/org/qtproject/qt5/android/SAFUtils.java 1440 @@ -0,0 +1,23 @@ 1441 +package org.qtproject.qt5.android; 1442 + 1443 +import android.database.Cursor; 1444 + 1445 +public class SAFUtils { 1446 + 1447 + public static String getColumnValStringOrNull(Cursor cursor, String column) { 1448 + int index = cursor.getColumnIndex(column); 1449 + if (index == -1) { 1450 + return null; 1451 + } 1452 + return cursor.getString(index); 1453 + } 1454 + 1455 + public static Integer getColumnValIntegerOrDefault(Cursor cursor, String column, int defaultVal) { 1456 + int index = cursor.getColumnIndex(column); 1457 + if (index == -1) { 1458 + return defaultVal; 1459 + } 1460 + return cursor.getInt(index); 1461 + } 1462 + 1463 +} 1464 diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp 1465 index ba11a50e85..b9ed95b768 100644 1466 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp 1467 +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp 1468 @@ -45,10 +45,17 @@ 1469 #include <QDebug> 1470 1471 AndroidContentFileEngine::AndroidContentFileEngine(const QString &f) 1472 - : m_fd(-1), m_file(f), m_resolvedName(QString()) 1473 + : m_fd(-1) 1474 + , m_file(f) 1475 + , m_resolvedName(QString()) 1476 + , m_safFileManager(QJNIObjectPrivate::callStaticObjectMethod( 1477 + "org/qtproject/qt5/android/SAFFileManager", "instance", 1478 + "()Lorg/qtproject/qt5/android/SAFFileManager;")) 1479 + , m_safError(m_safFileManager.getObjectField("mError", 1480 + "Lorg/qtproject/qt5/android/FileError;")) 1481 { 1482 + m_resolvedName = getResolvedFileName(f); 1483 setFileName(f); 1484 - setResolvedFileName(f); 1485 } 1486 1487 bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) 1488 @@ -74,35 +81,60 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) 1489 openModeStr += QLatin1Char('a'); 1490 } 1491 1492 - const auto fd = QJNIObjectPrivate::callStaticMethod<jint>("org/qtproject/qt5/android/QtNative", 1493 - "openFdForContentUrl", 1494 - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)I", 1495 - QtAndroidPrivate::context(), 1496 + const auto fd = m_safFileManager.callMethod<jint>( 1497 + "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I", 1498 QJNIObjectPrivate::fromString(m_file).object(), 1499 QJNIObjectPrivate::fromString(openModeStr).object()); 1500 1501 if (fd < 0) { 1502 - setError(QFileDevice::OpenError, QLatin1String("The file could not be opened.")); 1503 + setErrorFromSAF(); 1504 return false; 1505 } 1506 1507 setFileDescriptor(fd); 1508 - return QFSFileEngine::open(openMode, m_fd, QFile::AutoCloseHandle); 1509 + return QFSFileEngine::open(openMode, m_fd, QFile::DontCloseHandle); 1510 } 1511 1512 bool AndroidContentFileEngine::close() 1513 { 1514 - return QJNIObjectPrivate::callStaticMethod<jboolean>( 1515 - "org/qtproject/qt5/android/QtNative", "closeFd", 1516 - "(I)Z", m_fd); 1517 + setErrorFromSAF(); 1518 + return m_safFileManager.callMethod<jboolean>("closeFileDescriptor", "(I)Z", m_fd); 1519 +} 1520 + 1521 +QJNIObjectPrivate toJavaUri(const QString &stringUri) 1522 +{ 1523 + const auto uri = QJNIObjectPrivate::callStaticObjectMethod( 1524 + "android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", 1525 + QJNIObjectPrivate::fromString(stringUri).object()); 1526 + 1527 + if (!uri.isValid()) { 1528 + qWarning("Invalid Uri returned"); 1529 + } 1530 + return uri; 1531 +} 1532 + 1533 +bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories) const 1534 +{ 1535 + return m_safFileManager.callMethod<jboolean>( 1536 + "mkdir", "(Ljava/lang/String;Z)Z", 1537 + QJNIObjectPrivate::fromString(dirName).object(), createParentDirectories); 1538 +} 1539 + 1540 +bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const 1541 +{ 1542 + if (recurseParentDirectories) { 1543 + qWarning() << "rmpath(): Unsupported"; 1544 + } 1545 + return m_safFileManager.callMethod<jboolean>( 1546 + "delete", "(Ljava/lang/String;)Z", 1547 + QJNIObjectPrivate::fromString(dirName).object()); 1548 } 1549 1550 qint64 AndroidContentFileEngine::size() const 1551 { 1552 - const jlong size = QJNIObjectPrivate::callStaticMethod<jlong>( 1553 - "org/qtproject/qt5/android/QtNative", "getSize", 1554 - "(Landroid/content/Context;Ljava/lang/String;)J", QtAndroidPrivate::context(), 1555 - QJNIObjectPrivate::fromString(m_file).object()); 1556 + const jlong size = m_safFileManager.callMethod<jlong>( 1557 + "getSize", "(Ljava/lang/String;)J", 1558 + QJNIObjectPrivate::fromString(m_file).object()); 1559 return (qint64)size; 1560 } 1561 1562 @@ -110,44 +142,65 @@ AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlag 1563 { 1564 const FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag); 1565 FileFlags flags; 1566 - const bool exists = QJNIObjectPrivate::callStaticMethod<jboolean>( 1567 - "org/qtproject/qt5/android/QtNative", "checkFileExists", 1568 - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), 1569 - QJNIObjectPrivate::fromString(m_file).object()); 1570 + const bool exists = 1571 + m_safFileManager.callMethod<jboolean>("exists", "(Ljava/lang/String;)Z", 1572 + QJNIObjectPrivate::fromString(m_file).object()); 1573 if (!exists) 1574 return flags; 1575 1576 - flags = FileType | commonFlags; 1577 + flags = commonFlags; 1578 1579 - const bool canWrite = QJNIObjectPrivate::callStaticMethod<jboolean>( 1580 - "org/qtproject/qt5/android/QtNative", "canWriteToUri", 1581 - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), 1582 - QJNIObjectPrivate::fromString(m_file).object()); 1583 + const bool canWrite = 1584 + m_safFileManager.callMethod<jboolean>("canWrite", "(Ljava/lang/String;)Z", 1585 + QJNIObjectPrivate::fromString(m_file).object()); 1586 if (canWrite) { 1587 flags |= (WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm); 1588 } 1589 + 1590 + const bool isDir = m_safFileManager.callMethod<jboolean>( 1591 + "isDir", "(Ljava/lang/String;)Z", QJNIObjectPrivate::fromString(m_file).object()); 1592 + if (isDir) { 1593 + flags = DirectoryType | flags; 1594 + } else { 1595 + flags = FileType | flags; 1596 + } 1597 return type & flags; 1598 } 1599 1600 QString AndroidContentFileEngine::fileName(FileName f) const 1601 { 1602 switch (f) { 1603 - case DefaultName: { 1604 + case DefaultName: { 1605 + // the file isn't created here, so the resolved filename is empty 1606 + if (m_resolvedName.isEmpty()) { 1607 + const int pos = m_file.lastIndexOf(QChar(QLatin1Char('/'))); 1608 + return m_file.mid(pos + 1); 1609 + } else { 1610 return m_resolvedName; 1611 } 1612 - case PathName: 1613 - case AbsoluteName: 1614 - case AbsolutePathName: 1615 - case CanonicalName: 1616 - case CanonicalPathName: 1617 - return m_file; 1618 - 1619 - case BaseName: { 1620 - const int pos = m_resolvedName.lastIndexOf(QChar(QLatin1Char('/'))); 1621 - return m_resolvedName.mid(pos); 1622 + } 1623 + case PathName: 1624 + case AbsoluteName: 1625 + case AbsolutePathName: 1626 + case CanonicalName: 1627 + return m_file; 1628 + case CanonicalPathName: { 1629 + const bool isTree = m_safFileManager.callMethod<jboolean>( 1630 + "isTreeUri", "(Ljava/lang/String;)Z", 1631 + QJNIObjectPrivate::fromString(m_file).object()); 1632 + 1633 + if (isTree) { 1634 + const int pos = m_file.lastIndexOf(QChar(QLatin1Char('/'))); 1635 + return m_file.left(pos); 1636 } 1637 - default: 1638 - return QString(); 1639 + return m_file; 1640 + } 1641 + case BaseName: { 1642 + const int pos = m_resolvedName.lastIndexOf(QChar(QLatin1Char('/'))); 1643 + return m_resolvedName.mid(pos); 1644 + } 1645 + default: 1646 + return QString(); 1647 } 1648 } 1649 1650 @@ -161,32 +214,87 @@ bool AndroidContentFileEngine::isRelativePath() const 1651 } 1652 } 1653 1654 -void AndroidContentFileEngine::setResolvedFileName(const QString& uri) 1655 +bool AndroidContentFileEngine::rename(const QString &newName) 1656 +{ 1657 + auto renameSaf = [this](QString newName) -> bool { 1658 + return m_safFileManager.callMethod<jboolean>( 1659 + "rename", "(Ljava/lang/String;Ljava/lang/String;)Z", 1660 + QJNIObjectPrivate::fromString(m_file).object(), 1661 + QJNIObjectPrivate::fromString(newName).object()); 1662 + }; 1663 + 1664 + // if the file doesn't have scheme it means the newName is only the fileName part 1665 + if (!newName.startsWith("content://")) { 1666 + return renameSaf(newName); 1667 + } 1668 + 1669 + auto getPos = [](QString file) { 1670 + int posDecoded = file.lastIndexOf(QLatin1String("/")); 1671 + int posEncoded = file.lastIndexOf(QLatin1String("%2F")); 1672 + return posEncoded > posDecoded ? posEncoded : posDecoded; 1673 + }; 1674 + 1675 + const QString parent = m_file.left(getPos(m_file)); 1676 + 1677 + if (newName.contains(parent)) { 1678 + const int pos = getPos(newName); 1679 + const QString displayName = newName.mid(pos + 1); 1680 + 1681 + return renameSaf(displayName); 1682 + } 1683 + 1684 + m_resolvedName = getResolvedFileName(m_file); 1685 + return false; 1686 +} 1687 + 1688 +bool AndroidContentFileEngine::remove() 1689 { 1690 - QJNIObjectPrivate resolvedName = QJNIObjectPrivate::callStaticObjectMethod( 1691 - "org/qtproject/qt5/android/QtNative", 1692 - "getFileNameFromUri", 1693 - "(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;", 1694 - QtAndroidPrivate::context(), 1695 - QJNIObjectPrivate::fromString(uri).object()); 1696 + return m_safFileManager.callMethod<jboolean>( 1697 + "delete", "(Ljava/lang/String;)Z", 1698 + QJNIObjectPrivate::fromString(m_file).object()); 1699 +} 1700 + 1701 +QString AndroidContentFileEngine::getResolvedFileName(const QString &path) const 1702 +{ 1703 + QJNIObjectPrivate resolvedName = m_safFileManager.callObjectMethod( 1704 + "getFileName", "(Ljava/lang/String;)Ljava/lang/String;", 1705 + QJNIObjectPrivate::fromString(path).object()); 1706 1707 if (resolvedName.isValid()) { 1708 - m_resolvedName = resolvedName.toString(); 1709 - } else { 1710 - qWarning("setResolvedFileName: Couldn't resolve the URI"); 1711 + return resolvedName.toString(); 1712 } 1713 + return QString(); 1714 +} 1715 + 1716 +QAbstractFileEngine::Iterator * 1717 +AndroidContentFileEngine::beginEntryList(QDir::Filters filters, 1718 + const QStringList &filterNames) 1719 +{ 1720 + return new AndroidContentFileEngineIterator(m_safFileManager, filters, filterNames); 1721 } 1722 1723 -void AndroidContentFileEngine::setFileDescriptor(const int fd) 1724 +QAbstractFileEngine::Iterator *AndroidContentFileEngine::endEntryList() 1725 { 1726 - m_fd = fd; 1727 + return nullptr; 1728 } 1729 1730 +void AndroidContentFileEngine::setFileDescriptor(const int fd) { m_fd = fd; } 1731 + 1732 +void AndroidContentFileEngine::setErrorFromSAF() 1733 +{ 1734 + auto error = m_safError.callMethod<jint>("getError"); 1735 + auto errorString = 1736 + m_safError.callObjectMethod("getErrorString", "()Ljava/lang/String;"); 1737 + if (errorString.isValid()) { 1738 + setError(static_cast<QFileDevice::FileError>(error), errorString.toString()); 1739 + } 1740 +} 1741 1742 AndroidContentFileEngineHandler::AndroidContentFileEngineHandler() = default; 1743 AndroidContentFileEngineHandler::~AndroidContentFileEngineHandler() = default; 1744 1745 -QAbstractFileEngine* AndroidContentFileEngineHandler::create(const QString &fileName) const 1746 +QAbstractFileEngine * 1747 +AndroidContentFileEngineHandler::create(const QString &fileName) const 1748 { 1749 if (!fileName.startsWith(QLatin1String("content"))) { 1750 return nullptr; 1751 @@ -194,3 +302,63 @@ QAbstractFileEngine* AndroidContentFileEngineHandler::create(const QString &file 1752 1753 return new AndroidContentFileEngine(fileName); 1754 } 1755 + 1756 +AndroidContentFileEngineIterator::AndroidContentFileEngineIterator( 1757 + QJNIObjectPrivate safFileManager, QDir::Filters filters, 1758 + const QStringList &filterNames) 1759 + : QAbstractFileEngineIterator(filters, filterNames) 1760 + , m_safFileManager(safFileManager) 1761 +{ 1762 +} 1763 + 1764 +AndroidContentFileEngineIterator::~AndroidContentFileEngineIterator() 1765 +{ 1766 + m_safFileManager.callMethod<void>("resetListCache"); 1767 +} 1768 + 1769 +QString AndroidContentFileEngineIterator::next() 1770 +{ 1771 + if (!hasNext()) { 1772 + return QString(); 1773 + } 1774 + m_index++; 1775 + 1776 + // just like it is in QFSFileEngineIterator 1777 + return currentFilePath(); 1778 +} 1779 + 1780 +bool AndroidContentFileEngineIterator::hasNext() const 1781 +{ 1782 + if (m_index == -2) { 1783 + fetchEntries(); 1784 + m_index++; 1785 + } 1786 + return m_index < m_entries.size() && m_entries.size() > 0; 1787 +} 1788 + 1789 +QString AndroidContentFileEngineIterator::currentFileName() const 1790 +{ 1791 + if (!hasNext()) { 1792 + return QString(); 1793 + } 1794 + return m_entries.at(m_index); 1795 +} 1796 + 1797 +void AndroidContentFileEngineIterator::fetchEntries() const 1798 +{ 1799 + QJNIObjectPrivate fileNames = m_safFileManager.callObjectMethod( 1800 + "listFileNames", "(Ljava/lang/String;)[Ljava/lang/String;", 1801 + QJNIObjectPrivate::fromString(path()).object()); 1802 + 1803 + if (!fileNames.isValid()) { 1804 + return; 1805 + } 1806 + 1807 + QJNIEnvironmentPrivate env; 1808 + const jsize length = env->GetArrayLength(static_cast<jarray>(fileNames.object())); 1809 + for (int i = 0; i < length; ++i) { 1810 + QJNIObjectPrivate elem( 1811 + env->GetObjectArrayElement(static_cast<jobjectArray>(fileNames.object()), i)); 1812 + m_entries << elem.toString(); 1813 + } 1814 +} 1815 diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h 1816 index 6769352ffd..8c120f8023 100644 1817 --- a/src/plugins/platforms/android/androidcontentfileengine.h 1818 +++ b/src/plugins/platforms/android/androidcontentfileengine.h 1819 @@ -41,6 +41,9 @@ 1820 #define ANDROIDCONTENTFILEENGINE_H 1821 1822 #include <private/qfsfileengine_p.h> 1823 +#include <private/qjni_p.h> 1824 + 1825 +class AndroidContentFileEngineIterator; 1826 1827 class AndroidContentFileEngine : public QFSFileEngine 1828 { 1829 @@ -48,21 +51,32 @@ public: 1830 AndroidContentFileEngine(const QString &fileName); 1831 bool open(QIODevice::OpenMode openMode) override; 1832 bool close() override; 1833 + bool mkdir(const QString &dirName, bool createParentDirectories) const override; 1834 + bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; 1835 qint64 size() const override; 1836 FileFlags fileFlags(FileFlags type = FileInfoAll) const override; 1837 QString fileName(FileName file = DefaultName) const override; 1838 bool isRelativePath() const override; 1839 + bool rename(const QString &newName) override; 1840 + bool remove() override; 1841 + 1842 + QString getResolvedFileName(const QString &file) const; 1843 1844 - /// Resolves the URI to the actual filename 1845 - void setResolvedFileName(const QString& uri); 1846 + QAbstractFileEngine::Iterator * 1847 + beginEntryList(QDir::Filters filters, const QStringList &filterNames) override; 1848 + QAbstractFileEngine::Iterator *endEntryList() override; 1849 1850 private: 1851 void setFileDescriptor(const int fd); 1852 1853 + void setErrorFromSAF(); 1854 + 1855 private: 1856 int m_fd; 1857 QString m_file; 1858 QString m_resolvedName; 1859 + QJNIObjectPrivate m_safFileManager; 1860 + QJNIObjectPrivate m_safError; 1861 }; 1862 1863 class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler 1864 @@ -73,4 +87,25 @@ public: 1865 QAbstractFileEngine *create(const QString &fileName) const override; 1866 }; 1867 1868 +class AndroidContentFileEngineIterator : public QAbstractFileEngineIterator { 1869 +public: 1870 + AndroidContentFileEngineIterator(QJNIObjectPrivate safFileManager, 1871 + QDir::Filters filters, 1872 + const QStringList &filterNames); 1873 + ~AndroidContentFileEngineIterator(); 1874 + 1875 + QString next() override; 1876 + bool hasNext() const override; 1877 + QString currentFileName() const override; 1878 + 1879 +private: 1880 + void fetchEntries() const; 1881 + 1882 +private: 1883 + 1884 + mutable QStringList m_entries; 1885 + mutable int m_index = -2; 1886 + QJNIObjectPrivate m_safFileManager; 1887 +}; 1888 + 1889 #endif // ANDROIDCONTENTFILEENGINE_H 1890 diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp 1891 index 00b5b0887c..e7068d369a 100644 1892 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp 1893 +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp 1894 @@ -119,6 +119,12 @@ void QAndroidPlatformFileDialogHelper::takePersistableUriPermission(const QJNIOb 1895 "getContentResolver", "()Landroid/content/ContentResolver;"); 1896 contentResolver.callMethod<void>("takePersistableUriPermission", "(Landroid/net/Uri;I)V", 1897 uri.object(), modeFlags); 1898 + 1899 + QJNIObjectPrivate safFileManager = QJNIObjectPrivate::callStaticObjectMethod( 1900 + "org/qtproject/qt5/android/SAFFileManager", "instance", 1901 + "()Lorg/qtproject/qt5/android/SAFFileManager;"); 1902 + 1903 + safFileManager.callMethod<void>("resetCachedPermission"); 1904 } 1905 1906 void QAndroidPlatformFileDialogHelper::setIntentTitle(const QString &title) 1907 diff --git a/src/plugins/platforms/android/qandroidplatformservices.cpp b/src/plugins/platforms/android/qandroidplatformservices.cpp 1908 index c095613ce7..5abfc7bbb5 100644 1909 --- a/src/plugins/platforms/android/qandroidplatformservices.cpp 1910 +++ b/src/plugins/platforms/android/qandroidplatformservices.cpp 1911 @@ -68,10 +68,13 @@ bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) 1912 1913 QJNIObjectPrivate urlString = QJNIObjectPrivate::fromString(url.toString()); 1914 QJNIObjectPrivate mimeString = QJNIObjectPrivate::fromString(mime); 1915 - return QJNIObjectPrivate::callStaticMethod<jboolean>( 1916 - QtAndroid::applicationClass(), "openURL", 1917 - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z", 1918 - QtAndroidPrivate::context(), urlString.object(), mimeString.object()); 1919 + QJNIObjectPrivate safFileManager = QJNIObjectPrivate::callStaticObjectMethod( 1920 + "org/qtproject/qt5/android/SAFFileManager", "instance", 1921 + "()Lorg/qtproject/qt5/android/SAFFileManager;"); 1922 + 1923 + return safFileManager.callMethod<jboolean>("launchUri", 1924 + "(Ljava/lang/String;Ljava/lang/String;)Z", 1925 + urlString.object(), mimeString.object()); 1926 } 1927 1928 bool QAndroidPlatformServices::openDocument(const QUrl &url) 1929 -- 1930 2.34.1 1931