[graphics/krita/krita/6.0] libs: [android] Fix identical animation frame copying

Carsten Hartenfels <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 9e42f95af3363fed6c10e95ce21067fe85cfbd3b by Carsten Hartenfels.
Committed on 27/07/2026 at 17:19.
Pushed by hartenfels into branch 'krita/6.0'.

[android] Fix identical animation frame copying

It used QFile::copy, which is busted for sandboxed directories. It now
goes through QFile manually, since that handles content URIs correctly.

M  +71   -0    libs/global/KisAndroidUtils.cpp
M  +6    -0    libs/global/KisAndroidUtils.h
M  +22   -1    libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp
M  +4    -56   libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
M  +0    -2    libs/ui/animation/KisAndroidMediaEncoderRunnable.h

https://invent.kde.org/graphics/krita/-/commit/9e42f95af3363fed6c10e95ce21067fe85cfbd3b

diff --git a/libs/global/KisAndroidUtils.cpp b/libs/global/KisAndroidUtils.cpp
index 2a50e2b30f8..2856695ca5a 100644
--- a/libs/global/KisAndroidUtils.cpp
+++ b/libs/global/KisAndroidUtils.cpp
@@ -5,6 +5,8 @@
 #include "KisAndroidLogHandler.h"
 #include <kis_debug.h>
 
+#include <QFile>
+
 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
 #include <QJniEnvironment>
 #include <QJniObject>
@@ -99,4 +101,73 @@ void setFullScreen(bool fullScreen)
     }
 }
 
+bool copyFile(const QString &inputPath, const QString &outputPath, QString *outErrorMessage)
+{
+    QFile inputFile(inputPath);
+    if (!inputFile.open(QIODevice::ReadOnly)) {
+        if (outErrorMessage) {
+            *outErrorMessage =
+                QStringLiteral("failed to open input file '%1': %2").arg(inputPath).arg(inputFile.errorString());
+        }
+        return false;
+    }
+
+    QFile outputFile(outputPath);
+    if (!outputFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+        if (outErrorMessage) {
+            *outErrorMessage =
+                QStringLiteral("failed to open output file '%1': %2").arg(outputPath).arg(outputFile.errorString());
+        }
+        return false;
+    }
+
+    QByteArray buffer;
+    buffer.resize(BUFSIZ);
+    while (true) {
+        qint64 read = inputFile.read(buffer.data(), BUFSIZ);
+        if (read < 0) {
+            if (outErrorMessage) {
+                *outErrorMessage = QStringLiteral("failed to read from input file '%1': %2")
+                                       .arg(inputPath)
+                                       .arg(inputFile.errorString());
+            }
+            return false;
+        } else if (read > 0) {
+            qint64 written = outputFile.write(buffer, read);
+            if (written < 0) {
+                if (outErrorMessage) {
+                    *outErrorMessage = QStringLiteral("failed to write %1 byte(s) to output file '%2': %3")
+                                           .arg(read)
+                                           .arg(outputPath)
+                                           .arg(outputFile.errorString());
+                }
+                return false;
+            } else if (written != read) {
+                if (outErrorMessage) {
+                    *outErrorMessage =
+                        QStringLiteral("tried to write %1 byte(s) to output file '%2', but only wrote %3")
+                            .arg(read)
+                            .arg(outputPath)
+                            .arg(written);
+                }
+                return false;
+            }
+        } else {
+            if (outputFile.flush()) {
+                if (outErrorMessage) {
+                    outErrorMessage->clear();
+                }
+                return true;
+            } else {
+                if (outErrorMessage) {
+                    *outErrorMessage = QStringLiteral("failed to flush output file '%1': %2")
+                                           .arg(outputPath)
+                                           .arg(outputFile.errorString());
+                }
+                return false;
+            }
+        }
+    }
+}
+
 } // namespace KisAndroidUtils
diff --git a/libs/global/KisAndroidUtils.h b/libs/global/KisAndroidUtils.h
index 2f9976963a2..3bf828877d9 100644
--- a/libs/global/KisAndroidUtils.h
+++ b/libs/global/KisAndroidUtils.h
@@ -34,6 +34,12 @@ KRITAGLOBAL_EXPORT bool isInFullScreen();
 // Enters or exits immersive mode if we're not in that state already.
 KRITAGLOBAL_EXPORT void setFullScreen(bool fullScreen);
 
+// QFile::copy doesn't work on sandboxed directories, use this instead. The
+// value placed in outErrorMessage is not translated, use it for logging or
+// present it to the user as an internal error. On success, it will be cleared.
+KRITAGLOBAL_EXPORT bool
+copyFile(const QString &inputPath, const QString &outputPath, QString *outErrorMessage = nullptr);
+
 } // namespace KisAndroidUtils
 
 #endif // __KISANDROIDUTILS_H_
diff --git a/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp b/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp
index 9161b4fcad4..c95d42a5b4c 100644
--- a/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp
+++ b/libs/ui/KisAsyncAnimationFramesSavingRenderer.cpp
@@ -14,6 +14,12 @@
 #include "kis_time_span.h"
 #include "kis_paint_layer.h"
 
+#include <kis_debug.h>
+
+#ifdef Q_OS_ANDROID
+#include <KisAndroidUtils.h>
+#endif
+
 
 struct KisAsyncAnimationFramesSavingRenderer::Private
 {
@@ -113,7 +119,22 @@ void KisAsyncAnimationFramesSavingRenderer::frameCompletedCallback(int frame, co
                 QString identicalFrameNumber = QString("%1").arg(identicalFrame + m_d->sequenceNumberingOffset, 4, 10, QChar('0'));
                 QString identicalFrameName = m_d->filenamePrefix + identicalFrameNumber + m_d->filenameSuffix;
 
-                if (!QFile::copy(filename, identicalFrameName)) {
+                bool copyOk;
+                QString copyErrorMessage;
+#ifdef Q_OS_ANDROID
+                copyOk = KisAndroidUtils::copyFile(filename, identicalFrameName, &copyErrorMessage);
+#else
+                QFile sourceFile(filename);
+                if (sourceFile.copy(identicalFrameName)) {
+                    copyOk = true;
+                } else {
+                    copyOk = false;
+                    copyErrorMessage = sourceFile.errorString();
+                }
+#endif
+                if (!copyOk) {
+                    warnFile.nospace() << "Failed to copy frame '" << filename << "' to '" << identicalFrameName
+                                       << "': " << copyErrorMessage;
                     status = ImportExportCodes::ErrorWhileWriting;
                     break;
                 }
diff --git a/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp b/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
index a0dc30f2b1e..06ed3515e4e 100644
--- a/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
+++ b/libs/ui/animation/KisAndroidMediaEncoderRunnable.cpp
@@ -23,6 +23,7 @@ using QJniObject = QAndroidJniObject;
 
 #include <klocalizedstring.h>
 
+#include <KisAndroidUtils.h>
 #include <kis_debug.h>
 
 extern "C" {
@@ -610,7 +611,9 @@ KisMediaEncoderRunnable::EncodeResult KisAndroidMediaEncoderRunnable::encode(QSt
         }
 
         if (closeResult == STATUS_NEEDS_COPY) {
-            if (!copyTemporaryToOutputFile(ctx, tempFilePath, settings().outputFile)) {
+            QString copyErrorMessage;
+            if (!KisAndroidUtils::copyFile(tempFilePath, settings().outputFile, &copyErrorMessage)) {
+                ctx.setInternalErrorMessage(copyErrorMessage);
                 return EncodeResult::Failed;
             }
         }
@@ -679,61 +682,6 @@ int KisAndroidMediaEncoderRunnable::drain(Context &ctx, long long initialTimeout
     return count;
 }
 
-bool KisAndroidMediaEncoderRunnable::copyTemporaryToOutputFile(Context &ctx,
-                                                               const QString &tempPath,
-                                                               const QString &outputPath)
-{
-    QFile tempFile(tempPath);
-    if (!tempFile.open(QIODevice::ReadOnly)) {
-        ctx.setInternalErrorMessage(
-            QStringLiteral("failed to open temp file '%1': %2").arg(tempPath).arg(tempFile.errorString()));
-        return false;
-    }
-
-    QFile outputFile(outputPath);
-    if (!outputFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
-        ctx.setInternalErrorMessage(
-            QStringLiteral("failed to open output file '%1': %2").arg(outputPath).arg(outputFile.errorString()));
-        return false;
-    }
-
-    QByteArray buffer;
-    buffer.resize(BUFSIZ);
-    while (true) {
-        qint64 read = tempFile.read(buffer.data(), BUFSIZ);
-        if (read < 0) {
-            ctx.setInternalErrorMessage(
-                QStringLiteral("failed to read from temp file '%1': %2").arg(tempPath).arg(tempFile.errorString()));
-            return false;
-        } else if (read > 0) {
-            qint64 written = outputFile.write(buffer, read);
-            if (written < 0) {
-                ctx.setInternalErrorMessage(QStringLiteral("failed to write %1 byte(s) to output file '%2': %3")
-                                                .arg(read)
-                                                .arg(outputPath)
-                                                .arg(outputFile.errorString()));
-                return false;
-            } else if (written != read) {
-                ctx.setInternalErrorMessage(
-                    QStringLiteral("tried to write %1 byte(s) to output file '%2', but only wrote %3")
-                        .arg(read)
-                        .arg(outputPath)
-                        .arg(written));
-                return false;
-            }
-        } else {
-            if (outputFile.flush()) {
-                return true;
-            } else {
-                ctx.setInternalErrorMessage(QStringLiteral("failed to flush output file '%1': %2")
-                                                .arg(outputPath)
-                                                .arg(outputFile.errorString()));
-                return false;
-            }
-        }
-    }
-}
-
 bool KisAndroidMediaEncoderRunnable::readEncoderImage(Context &ctx, EncoderImage &outImage)
 {
     return readPlaneBuffer(ctx, 0, outImage.bufferY) && readPlaneBuffer(ctx, 1, outImage.bufferU)
diff --git a/libs/ui/animation/KisAndroidMediaEncoderRunnable.h b/libs/ui/animation/KisAndroidMediaEncoderRunnable.h
index 482225ffc48..d9ecd39b95e 100644
--- a/libs/ui/animation/KisAndroidMediaEncoderRunnable.h
+++ b/libs/ui/animation/KisAndroidMediaEncoderRunnable.h
@@ -48,8 +48,6 @@ private:
     // Returns number of frames drained or one of the DRAIN_* values above.
     int drain(Context &ctx, long long initialTimeout);
 
-    bool copyTemporaryToOutputFile(Context &ctx, const QString &tempPath, const QString &outputPath);
-
     static bool readEncoderImage(Context &ctx, EncoderImage &outImage);
     static bool readPlaneBuffer(Context &ctx, int index, uint8_t *&outBuffer);
     static bool readPlaneRowStride(Context &ctx, int index, int &outRowStride);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.