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