Commit de8b8604 authored by Daniel Teske's avatar Daniel Teske
Browse files

AndroidManifestEditor: Rewrite writing of xml files



Using QXmlStreamWriter + QXmlStreamReader gives us greater control
over the ordering of attributes. That way we can ensure that we never
change the ordering that the user used. (Whereas for QDom the best
we can do is to have a predictable but random order.)

Neither QXmlStreamWriter nor QDom allows us to control the indentation
and other whitespace, so any user indentation is lost.

In addtion QXmlStreamWriter seems to have several bugs with indentation
though, leading to rather strangely formatted files.

Task-number: QTCREATORBUG-10870

Change-Id: I4cdbdcd499227f418e7767eb1b532efbbd5083a5
Reviewed-by: default avatarEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@digia.com>
Reviewed-by: default avatarAlessandro Portale <alessandro.portale@digia.com>
parent 8303f793
......@@ -805,53 +805,6 @@ void AndroidManifestEditorWidget::syncToWidgets(const QDomDocument &doc)
m_dirty = false;
}
void setUsesSdk(QDomDocument &doc, QDomElement &manifest, int minimumSdk, int targetSdk)
{
QDomElement usesSdk = manifest.firstChildElement(QLatin1String("uses-sdk"));
if (usesSdk.isNull()) { // doesn't exist yet
if (minimumSdk == 0 && targetSdk == 0) {
// and doesn't need to exist
} else {
usesSdk = doc.createElement(QLatin1String("uses-sdk"));
if (minimumSdk != 0)
usesSdk.setAttribute(QLatin1String("android:minSdkVersion"), minimumSdk);
if (targetSdk != 0)
usesSdk.setAttribute(QLatin1String("android:targetSdkVersion"), targetSdk);
manifest.appendChild(usesSdk);
}
} else {
if (minimumSdk == 0 && targetSdk == 0) {
// We might be able to remove the whole element
// check if there are other attributes
QDomNamedNodeMap usesSdkAttributes = usesSdk.attributes();
bool keepNode = false;
for (int i = 0; i < usesSdkAttributes.size(); ++i) {
if (usesSdkAttributes.item(i).nodeName() != QLatin1String("android:minSdkVersion")
&& usesSdkAttributes.item(i).nodeName() != QLatin1String("android:targetSdkVersion")) {
keepNode = true;
break;
}
}
if (keepNode) {
usesSdk.removeAttribute(QLatin1String("android:minSdkVersion"));
usesSdk.removeAttribute(QLatin1String("android:targetSdkVersion"));
} else {
manifest.removeChild(usesSdk);
}
} else {
if (minimumSdk == 0)
usesSdk.removeAttribute(QLatin1String("android:minSdkVersion"));
else
usesSdk.setAttribute(QLatin1String("android:minSdkVersion"), minimumSdk);
if (targetSdk == 0)
usesSdk.removeAttribute(QLatin1String("android:targetSdkVersion"));
else
usesSdk.setAttribute(QLatin1String("android:targetSdkVersion"), targetSdk);
}
}
}
int extractVersion(const QString &string)
{
if (!string.startsWith(QLatin1String("API")))
......@@ -868,78 +821,330 @@ int extractVersion(const QString &string)
void AndroidManifestEditorWidget::syncToEditor()
{
QDomDocument doc;
if (!doc.setContent(m_textEditorWidget->toPlainText())) {
// This should not happen
updateInfoBar();
return;
QString result;
QXmlStreamReader reader(m_textEditorWidget->toPlainText());
reader.setNamespaceProcessing(false);
QXmlStreamWriter writer(&result);
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(4);
while (!reader.atEnd()) {
reader.readNext();
if (reader.hasError()) {
// This should not happen
updateInfoBar();
return;
} else {
if (reader.name() == QLatin1String("manifest"))
parseManifest(reader, writer);
else if (reader.isStartElement())
parseUnknownElement(reader, writer);
else
writer.writeCurrentToken(reader);
}
}
QDomElement manifest = doc.documentElement();
manifest.setAttribute(QLatin1String("package"), m_packageNameLineEdit->text());
manifest.setAttribute(QLatin1String("android:versionCode"), m_versionCode->value());
manifest.setAttribute(QLatin1String("android:versionName"), m_versionNameLinedit->text());
if (result == m_textEditorWidget->toPlainText())
return;
if (!m_appNameInStringsXml) {
QDomElement application = manifest.firstChildElement(QLatin1String("application"));
application.setAttribute(QLatin1String("android:label"), m_appNameLineEdit->text());
}
m_textEditorWidget->setPlainText(result);
m_textEditorWidget->document()->setModified(true);
setUsesSdk(doc, manifest, extractVersion(m_androidMinSdkVersion->currentText()),
extractVersion(m_androidTargetSdkVersion->currentText()));
m_dirty = false;
}
setAndroidAppLibName(doc, manifest.firstChildElement(QLatin1String("application"))
.firstChildElement(QLatin1String("activity")),
m_targetLineEdit->currentText());
namespace {
QXmlStreamAttributes modifyXmlStreamAttributes(const QXmlStreamAttributes &input, const QStringList &keys,
const QStringList values, const QStringList &remove = QStringList())
{
Q_ASSERT(keys.size() == values.size());
QXmlStreamAttributes result;
result.reserve(input.size());
foreach (const QXmlStreamAttribute &attribute, input) {
const QString &name = attribute.qualifiedName().toString();
if (remove.contains(name))
continue;
int index = keys.indexOf(name);
if (index == -1)
result.push_back(attribute);
else
result.push_back(QXmlStreamAttribute(name,
values.at(index)));
}
// permissions
QDomElement permissionElem = manifest.firstChildElement(QLatin1String("uses-permission"));
while (!permissionElem.isNull()) {
manifest.removeChild(permissionElem);
permissionElem = manifest.firstChildElement(QLatin1String("uses-permission"));
for (int i = 0; i < keys.size(); ++i) {
if (!result.hasAttribute(keys.at(i)))
result.push_back(QXmlStreamAttribute(keys.at(i), values.at(i)));
}
return result;
}
} // end namespace
void AndroidManifestEditorWidget::parseManifest(QXmlStreamReader &reader, QXmlStreamWriter &writer)
{
Q_ASSERT(reader.isStartElement());
writer.writeStartElement(reader.name().toString());
QXmlStreamAttributes attributes = reader.attributes();
QStringList keys = QStringList()
<< QLatin1String("package")
<< QLatin1String("android:versionCode")
<< QLatin1String("android:versionName");
QStringList values = QStringList()
<< m_packageNameLineEdit->text()
<< QString::number(m_versionCode->value())
<< m_versionNameLinedit->text();
QXmlStreamAttributes result = modifyXmlStreamAttributes(attributes, keys, values);
writer.writeAttributes(result);
QSet<QString> permissions = m_permissionsModel->permissions().toSet();
bool foundUsesSdk = false;
reader.readNext();
while (!reader.atEnd()) {
if (reader.name() == QLatin1String("application")) {
parseApplication(reader, writer);
} else if (reader.name() == QLatin1String("uses-sdk")) {
parseUsesSdk(reader, writer);
foundUsesSdk = true;
} else if (reader.name() == QLatin1String("uses-permission")) {
permissions.remove(parseUsesPermission(reader, writer, permissions));
} else if (reader.isEndElement()) {
if (!foundUsesSdk) {
int minimumSdk = extractVersion(m_androidMinSdkVersion->currentText());
int targetSdk = extractVersion(m_androidTargetSdkVersion->currentText());
if (minimumSdk == 0 && targetSdk == 0) {
// and doesn't need to exist
} else {
writer.writeEmptyElement(QLatin1String("uses-sdk"));
if (minimumSdk != 0)
writer.writeAttribute(QLatin1String("android:minSdkVersion"),
QString::number(minimumSdk));
if (targetSdk != 0)
writer.writeAttribute(QLatin1String("android:targetSdkVersion"),
QString::number(targetSdk));
}
}
if (!permissions.isEmpty()) {
foreach (const QString &permission, permissions) {
writer.writeEmptyElement(QLatin1String("uses-permission"));
writer.writeAttribute(QLatin1String("android:name"), permission);
}
}
foreach (const QString &permission, m_permissionsModel->permissions()) {
permissionElem = doc.createElement(QLatin1String("uses-permission"));
permissionElem.setAttribute(QLatin1String("android:name"), permission);
manifest.appendChild(permissionElem);
writer.writeCurrentToken(reader);
return;
} else if (reader.isStartElement()) {
parseUnknownElement(reader, writer);
} else {
writer.writeCurrentToken(reader);
}
reader.readNext();
}
}
void AndroidManifestEditorWidget::parseApplication(QXmlStreamReader &reader, QXmlStreamWriter &writer)
{
Q_ASSERT(reader.isStartElement());
writer.writeStartElement(reader.name().toString());
QXmlStreamAttributes attributes = reader.attributes();
QStringList keys;
QStringList values;
if (!m_appNameInStringsXml) {
keys << QLatin1String("android:label");
values << m_appNameLineEdit->text();
}
bool ensureIconAttribute = !m_lIconPath.isEmpty()
|| !m_mIconPath.isEmpty()
|| !m_hIconPath.isEmpty();
if (ensureIconAttribute) {
QDomElement applicationElem = manifest.firstChildElement(QLatin1String("application"));
applicationElem.setAttribute(QLatin1String("android:icon"), QLatin1String("@drawable/icon"));
keys << QLatin1String("android:icon");
values << QLatin1String("@drawable/icon");
}
QXmlStreamAttributes result = modifyXmlStreamAttributes(attributes, keys, values);
writer.writeAttributes(result);
QString newText = doc.toString(4);
if (newText == m_textEditorWidget->toPlainText())
return;
reader.readNext();
m_textEditorWidget->setPlainText(newText);
m_textEditorWidget->document()->setModified(true);
while (!reader.atEnd()) {
if (reader.isEndElement()) {
writer.writeCurrentToken(reader);
return;
} else if (reader.isStartElement()) {
if (reader.name() == QLatin1String("activity"))
parseActivity(reader, writer);
else
parseUnknownElement(reader, writer);
} else {
writer.writeCurrentToken(reader);
}
m_dirty = false;
reader.readNext();
}
}
bool AndroidManifestEditorWidget::setAndroidAppLibName(QDomDocument document, QDomElement activity, const QString &name)
void AndroidManifestEditorWidget::parseActivity(QXmlStreamReader &reader, QXmlStreamWriter &writer)
{
QDomElement metadataElem = activity.firstChildElement(QLatin1String("meta-data"));
while (!metadataElem.isNull()) {
if (metadataElem.attribute(QLatin1String("android:name")) == QLatin1String("android.app.lib_name")) {
metadataElem.setAttribute(QLatin1String("android:value"), name);
return true;
Q_ASSERT(reader.isStartElement());
writer.writeCurrentToken(reader);
reader.readNext();
bool found = false;
while (!reader.atEnd()) {
if (reader.isEndElement()) {
if (!found) {
writer.writeEmptyElement(QLatin1String("meta-data"));
writer.writeAttribute(QLatin1String("android:name"),
QLatin1String("android.app.lib_name"));
writer.writeAttribute(QLatin1String("android:value"),
m_targetLineEdit->currentText());
}
writer.writeCurrentToken(reader);
return;
} else if (reader.isStartElement()) {
if (reader.name() == QLatin1String("meta-data"))
found = parseMetaData(reader, writer) || found; // ORDER MATTERS
else
parseUnknownElement(reader, writer);
} else {
writer.writeCurrentToken(reader);
}
metadataElem = metadataElem.nextSiblingElement(QLatin1String("meta-data"));
reader.readNext();
}
}
bool AndroidManifestEditorWidget::parseMetaData(QXmlStreamReader &reader, QXmlStreamWriter &writer)
{
Q_ASSERT(reader.isStartElement());
bool found = false;
QXmlStreamAttributes attributes = reader.attributes();
QXmlStreamAttributes result;
if (attributes.value(QLatin1String("android:name")) == QLatin1String("android.app.lib_name")) {
QStringList keys = QStringList() << QLatin1String("android:value");
QStringList values = QStringList() << m_targetLineEdit->currentText();
result = modifyXmlStreamAttributes(attributes, keys, values);
found = true;
} else {
result = attributes;
}
writer.writeStartElement(QLatin1String("meta-data"));
writer.writeAttributes(result);
reader.readNext();
while (!reader.atEnd()) {
if (reader.isEndElement()) {
writer.writeCurrentToken(reader);
return found;
} else if (reader.isStartElement()) {
parseUnknownElement(reader, writer);
} else {
writer.writeCurrentToken(reader);
}
reader.readNext();
}
return found; // should never be reached
}
void AndroidManifestEditorWidget::parseUsesSdk(QXmlStreamReader &reader, QXmlStreamWriter & writer)
{
int minimumSdk = extractVersion(m_androidMinSdkVersion->currentText());
int targetSdk = extractVersion(m_androidTargetSdkVersion->currentText());
QStringList keys;
QStringList values;
QStringList remove;
if (minimumSdk == 0) {
remove << QLatin1String("android:minSdkVersion");
} else {
keys << QLatin1String("android:minSdkVersion");
values << QString::number(minimumSdk);
}
if (targetSdk == 0) {
remove << QLatin1String("android:targetSdkVersion");
} else {
keys << QLatin1String("android:targetSdkVersion");
values << QString::number(targetSdk);
}
QXmlStreamAttributes result = modifyXmlStreamAttributes(reader.attributes(),
keys, values, remove);
bool removeUseSdk = result.isEmpty();
if (!removeUseSdk) {
writer.writeStartElement(reader.name().toString());
writer.writeAttributes(result);
}
reader.readNext();
while (!reader.atEnd()) {
if (reader.isEndElement()) {
if (!removeUseSdk)
writer.writeCurrentToken(reader);
return;
} else {
if (removeUseSdk) {
removeUseSdk = false;
writer.writeStartElement(QLatin1String("uses-sdk"));
}
if (reader.isStartElement())
parseUnknownElement(reader, writer);
else
writer.writeCurrentToken(reader);
}
reader.readNext();
}
}
QString AndroidManifestEditorWidget::parseUsesPermission(QXmlStreamReader &reader, QXmlStreamWriter &writer, const QSet<QString> permissions)
{
Q_ASSERT(reader.isStartElement());
QString permissionName = reader.attributes().value(QLatin1String("android:name")).toString();
bool writePermission = permissions.contains(permissionName);
if (writePermission)
writer.writeCurrentToken(reader);
reader.readNext();
while (!reader.atEnd()) {
if (reader.isEndElement()) {
if (writePermission)
writer.writeCurrentToken(reader);
return permissionName;
} else if (reader.isStartElement()) {
parseUnknownElement(reader, writer);
} else {
writer.writeCurrentToken(reader);
}
reader.readNext();
}
return permissionName; // should not be reached
}
void AndroidManifestEditorWidget::parseUnknownElement(QXmlStreamReader &reader, QXmlStreamWriter &writer)
{
Q_ASSERT(reader.isStartElement());
writer.writeCurrentToken(reader);
reader.readNext();
while (!reader.atEnd()) {
if (reader.isEndElement()) {
writer.writeCurrentToken(reader);
return;
} else if (reader.isStartElement()) {
parseUnknownElement(reader, writer);
} else {
writer.writeCurrentToken(reader);
}
reader.readNext();
}
QDomElement elem = document.createElement(QLatin1String("meta-data"));
elem.setAttribute(QLatin1String("android:name"), QLatin1String("android.app.lib_name"));
elem.setAttribute(QLatin1String("android:value"), name);
activity.appendChild(elem);
return true;
}
QString AndroidManifestEditorWidget::iconPath(const QString &baseDir, IconDPI dpi)
......
......@@ -48,6 +48,8 @@ class QLineEdit;
class QListView;
class QSpinBox;
class QToolButton;
class QXmlStreamReader;
class QXmlStreamWriter;
QT_END_NAMESPACE
namespace Core { class IEditor; }
......@@ -137,7 +139,6 @@ private:
void syncToEditor();
bool checkDocument(QDomDocument doc, QString *errorMessage, int *errorLine, int *errorColumn);
bool setAndroidAppLibName(QDomDocument document, QDomElement activity, const QString &name);
enum IconDPI { LowDPI, MediumDPI, HighDPI };
QIcon icon(const QString &baseDir, IconDPI dpi);
QString iconPath(const QString &baseDir, IconDPI dpi);
......@@ -147,6 +148,14 @@ private:
void hideInfoBar();
Q_SLOT void updateTargetComboBox();
void parseManifest(QXmlStreamReader &reader, QXmlStreamWriter &writer);
void parseApplication(QXmlStreamReader &reader, QXmlStreamWriter &writer);
void parseActivity(QXmlStreamReader &reader, QXmlStreamWriter &writer);
bool parseMetaData(QXmlStreamReader &reader, QXmlStreamWriter &writer);
void parseUsesSdk(QXmlStreamReader &reader, QXmlStreamWriter &writer);
QString parseUsesPermission(QXmlStreamReader &reader, QXmlStreamWriter &writer, const QSet<QString> permissions);
void parseUnknownElement(QXmlStreamReader &reader, QXmlStreamWriter &writer);
bool m_dirty; // indicates that we need to call syncToEditor()
bool m_stayClean;
bool m_setAppName;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment