// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QApplication>
#include <QCheckBox>
#include <QDataStream>
#include <QDir>
#include <QFile>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QStandardPaths>
#include <QVBoxLayout>
#include <QWidget>
#include <QtOhosExtras>
struct Note
{
int priority = 0;
};
struct Fields
{
QLineEdit *number = nullptr;
QLineEdit *text = nullptr;
QLineEdit *tags = nullptr;
QLineEdit *pairFirst = nullptr;
QLineEdit *pairSecond = nullptr;
QLineEdit *notePriority = nullptr;
QLineEdit *numbers = nullptr;
QPlainTextEdit *assetContent = nullptr;
QLabel *syncProgress = nullptr;
};
QByteArray serializeNote(const Note ¬e)
{
QByteArray bytes;
QDataStream stream(&bytes, QIODevice::WriteOnly);
stream << note.priority;
return bytes;
}
Note deserializeNote(const QByteArray &bytes)
{
Note note;
QDataStream stream(bytes);
stream >> note.priority;
return note;
}
QLineEdit *addFieldRow(QVBoxLayout *layout, const QString &label, const QString &value)
{
auto *rowLayout = new QHBoxLayout();
rowLayout->addWidget(new QLabel(label));
auto *edit = new QLineEdit(value);
rowLayout->addWidget(edit);
layout->addLayout(rowLayout);
return edit;
}
QStringList splitList(const QString &text)
{
QStringList values;
for (const auto &token : text.split(QLatin1Char(','))) {
const auto trimmedToken = token.trimmed();
if (!trimmedToken.isEmpty())
values.append(trimmedToken);
}
return values;
}
QJsonArray toNumberArray(const QStringList &tokens)
{
QJsonArray numbers;
for (const auto &token : tokens)
numbers.append(token.toInt());
return numbers;
}
QSharedPointer<QFile> tryWriteAssetFile(const QString &fileName, const QString &content)
{
const auto dirPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(dirPath);
auto file = QSharedPointer<QFile>::create(QDir(dirPath).filePath(fileName));
if (!file->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qWarning(
"failed to open '%s': %s",
qUtf8Printable(file->fileName()), qUtf8Printable(file->errorString()));
return nullptr;
}
file->write(content.toUtf8());
file->close();
return file;
}
QSharedPointer<QtOhosExtras::QOhosDistributedDataObject> createObjectToMigrate(const Fields &fields)
{
const auto tags = splitList(fields.tags->text());
const Note note{fields.notePriority->text().toInt()};
const auto encodedNote = QString::fromLatin1(serializeNote(note).toBase64());
const QJsonObject pair{
{QStringLiteral("first"), fields.pairFirst->text()},
{QStringLiteral("second"), fields.pairSecond->text()},
};
auto object = QtOhosExtras::QOhosDistributedDataObject::create(QJsonObject{
{QStringLiteral("number"), fields.number->text().toInt()},
{QStringLiteral("text"), fields.text->text()},
{QStringLiteral("tags"), QJsonArray::fromStringList(tags)},
{QStringLiteral("pair"), pair},
{QStringLiteral("note"), encodedNote},
{QStringLiteral("numbers"), toNumberArray(splitList(fields.numbers->text()))},
});
const QString assetContent = QStringList{
QStringLiteral("number: ") + fields.number->text(),
QStringLiteral("text: ") + fields.text->text(),
QStringLiteral("tags: ") + fields.tags->text(),
QStringLiteral("pair.first: ") + fields.pairFirst->text(),
QStringLiteral("pair.second: ") + fields.pairSecond->text(),
QStringLiteral("note.priority: ") + fields.notePriority->text(),
QStringLiteral("numbers: ") + fields.numbers->text(),
}.join(QLatin1Char('\n'));
const auto assetFile = tryWriteAssetFile(QStringLiteral("fields.txt"), assetContent);
if (assetFile)
object->addAssets({assetFile});
return object;
}
QString readAssetFile(const QString &path)
{
if (path.isEmpty())
return {};
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
return QString::fromUtf8(file.readAll());
}
void applyObjectFields(const Fields &fields, QtOhosExtras::QOhosDistributedDataObject *object)
{
if (!object)
return;
const auto has = [&](const QString &key) {
const auto value = object->get(key);
return !value.isNull() && !value.isUndefined();
};
if (has(QStringLiteral("number")))
fields.number->setText(QString::number(object->get(QStringLiteral("number")).toInt()));
if (has(QStringLiteral("text")))
fields.text->setText(object->get(QStringLiteral("text")).toString());
if (has(QStringLiteral("tags"))) {
QStringList tags;
for (const auto &tag : object->get(QStringLiteral("tags")).toArray())
tags.append(tag.toString());
fields.tags->setText(tags.join(QLatin1Char(',')));
}
if (has(QStringLiteral("pair"))) {
const auto pair = object->get(QStringLiteral("pair")).toObject();
fields.pairFirst->setText(pair.value(QStringLiteral("first")).toString());
fields.pairSecond->setText(pair.value(QStringLiteral("second")).toString());
}
if (has(QStringLiteral("note"))) {
const auto note = deserializeNote(
QByteArray::fromBase64(object->get(QStringLiteral("note")).toString().toLatin1()));
fields.notePriority->setText(QString::number(note.priority));
}
if (has(QStringLiteral("numbers"))) {
QStringList numbers;
for (const auto &value : object->get(QStringLiteral("numbers")).toArray())
numbers.append(QString::number(value.toInt()));
fields.numbers->setText(numbers.join(QLatin1Char(',')));
}
const auto assets = object->assets();
if (!assets.isEmpty()) {
const auto assetPath = assets.first().toObject().value(QStringLiteral("path")).toString();
fields.assetContent->setPlainText(readAssetFile(assetPath));
}
}
QSharedPointer<QtOhosExtras::QOhosDistributedDataObject> restoreFirstObject(
const QList<QSharedPointer<QtOhosExtras::QOhosDistributedDataObject>> &restored, const Fields &fields)
{
if (restored.isEmpty())
return {};
const auto object = restored.first();
auto *objectPtr = object.get();
QObject::connect(
objectPtr, &QtOhosExtras::QOhosDistributedDataObject::statusChanged,
objectPtr,
[fields, objectPtr](const QString &, const QString &, auto status) {
if (status != QtOhosExtras::QOhosDistributedDataObject::Status::Offline)
applyObjectFields(fields, objectPtr);
});
QObject::connect(
objectPtr, &QtOhosExtras::QOhosDistributedDataObject::dataChanged,
objectPtr,
[fields, objectPtr](const QString &, const QStringList &) {
applyObjectFields(fields, objectPtr);
});
QObject::connect(
objectPtr, &QtOhosExtras::QOhosDistributedDataObject::progressChanged,
objectPtr,
[fields](const QString &, int progress) {
fields.syncProgress->setText(QStringLiteral("sync progress: %1%").arg(progress));
});
applyObjectFields(fields, objectPtr);
return object;
}
QLayout *createContinuationStateButtonsLayout(
QSharedPointer<QtOhosExtras::QOhosAbilityContext> ability)
{
auto *buttonsLayout = new QHBoxLayout();
auto *activateButton = new QPushButton(QLatin1String("Set Continuation Active"));
QObject::connect(
activateButton, &QPushButton::clicked,
[ability]() {
ability->setContinuationActive(true);
});
buttonsLayout->addWidget(activateButton);
auto *deactivateButton = new QPushButton(QLatin1String("Set Continuation Inactive"));
QObject::connect(
deactivateButton, &QPushButton::clicked,
[ability]() {
ability->setContinuationActive(false);
});
buttonsLayout->addWidget(deactivateButton);
return buttonsLayout;
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget window;
auto *mainLayout = new QVBoxLayout(&window);
auto *dataLayout = new QHBoxLayout();
dataLayout->addWidget(new QLabel(QLatin1String("Received On Continue Data:")));
auto *valueLabel = new QLabel(QLatin1String(""));
dataLayout->addWidget(valueLabel);
mainLayout->addLayout(dataLayout);
Fields fields;
fields.number = addFieldRow(mainLayout, QLatin1String("number"), QLatin1String("42"));
fields.text = addFieldRow(mainLayout, QLatin1String("text"), QLatin1String("hello"));
fields.tags = addFieldRow(mainLayout, QLatin1String("tags (a,b,c)"), QLatin1String("red,green,blue"));
fields.pairFirst = addFieldRow(mainLayout, QLatin1String("pair.first"), QLatin1String("left"));
fields.pairSecond = addFieldRow(mainLayout, QLatin1String("pair.second"), QLatin1String("right"));
fields.notePriority = addFieldRow(mainLayout, QLatin1String("note.priority"), QLatin1String("7"));
fields.numbers = addFieldRow(mainLayout, QLatin1String("numbers (1,2,3)"), QLatin1String("1,2,3"));
mainLayout->addWidget(new QLabel(QLatin1String("asset: fields.txt")));
fields.assetContent = new QPlainTextEdit();
fields.assetContent->setReadOnly(true);
mainLayout->addWidget(fields.assetContent);
fields.syncProgress = new QLabel(QLatin1String("sync progress: -"));
mainLayout->addWidget(fields.syncProgress);
auto *exitAfterMigrationCheckBox =
new QCheckBox(QLatin1String("Exit app on source device after migration"));
exitAfterMigrationCheckBox->setChecked(true);
mainLayout->addWidget(exitAfterMigrationCheckBox);
window.show();
auto launchWant = QtOhosExtras::QOhosAppContext::getAppLaunchWant();
auto onContinueData = QtOhosExtras::tryGetOnContinueData(launchWant);
if (onContinueData) {
valueLabel->setText(*onContinueData.get());
} else {
valueLabel->setText(QLatin1String(""));
}
auto ability = QtOhosExtras::QOhosAbilityContext::getInstanceForMainWindow(window.windowHandle());
mainLayout->addLayout(createContinuationStateButtonsLayout(ability));
QObject::connect(
ability.get(), &QtOhosExtras::QOhosUiAbilityContext::continueRequestReceived,
ability.get(),
[fields, exitAfterMigrationCheckBox, abilityPtr = ability.get()](auto context) {
auto *appContext = QtOhosExtras::QOhosAppContext::instance();
auto appVersionCode = appContext->getBundleInfo()->versionCode();
context->setExitAppOnSourceDeviceAfterMigration(exitAfterMigrationCheckBox->isChecked());
auto object = createObjectToMigrate(fields);
context->setResponseOnDistributedDataObjectsSaveResults(
{object},
abilityPtr,
[context, appVersionCode](const QList<bool> &results) {
const bool saved = !results.isEmpty() && !results.contains(false);
if (saved && context->sourceApplicationVersionCode() == appVersionCode) {
context->setAgreeResponse(QByteArray("Migration Test Data"));
} else {
context->setMismatchResponse();
}
});
});
QObject::connect(
ability.get(), &QtOhosExtras::QOhosUiAbilityContext::newWantInfoReceived,
valueLabel,
[fields, valueLabel, continuationObjectHandle = restoreFirstObject(QtOhosExtras::tryRestoreOnContinueDistributedDataObjects(launchWant), fields)](auto wantInfo) mutable {
auto onContinueData = QtOhosExtras::tryGetOnContinueData(wantInfo->want());
if (onContinueData) {
valueLabel->setText(*onContinueData.get());
} else {
valueLabel->setText(QLatin1String(""));
}
auto newWantContinuationObjectHandle = restoreFirstObject(
QtOhosExtras::tryRestoreOnContinueDistributedDataObjects(wantInfo->want()), fields);
if (newWantContinuationObjectHandle)
continuationObjectHandle = newWantContinuationObjectHandle;
});
return app.exec();
}