[graphics/krita] libs: [android] Fix identical animation frame copying
Carsten Hartenfels <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 19e877e3b2f84240a92e15fb9c4eed0bdf39e4dc by Carsten Hartenfels.
Committed on 27/07/2026 at 16:06.
Pushed by hartenfels into branch 'master'.
[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/19e877e3b2f84240a92e15fb9c4eed0bdf39e4dc
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, ©ErrorMessage);
+#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, ©ErrorMessage)) {
+ 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);