Unity 8
windowstatestorage.cpp
1 /*
2  * Copyright 2015-2016 Canonical Ltd.
3  * Copyright 2021 UBports Foundation
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation; version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "windowstatestorage.h"
19 
20 #include <QDebug>
21 #include <QDir>
22 #include <QMetaObject>
23 #include <QObject>
24 #include <QSqlQuery>
25 #include <QSqlError>
26 #include <QSqlResult>
27 #include <QStandardPaths>
28 #include <QRect>
29 #include <unity/shell/application/ApplicationInfoInterface.h>
30 
31 
32 class AsyncQuery: public QObject
33 {
34  Q_OBJECT
35 
36 public:
37  AsyncQuery(const QString &dbName):
38  m_dbName(dbName)
39  {
40  }
41 
42  ~AsyncQuery()
43  {
44  QSqlDatabase::removeDatabase(m_connectionName);
45  }
46 
47  Q_INVOKABLE const QString getDbName()
48  {
49  if (!m_ok) {
50  return "ERROR";
51  }
52  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
53  return connection.databaseName();
54  }
55 
56  Q_INVOKABLE bool initdb()
57  {
58  if (m_ok) {
59  return true;
60  }
61  QSqlDatabase connection = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
62  connection.setDatabaseName(m_dbName);
63  connection.setConnectOptions(QStringLiteral("QSQLITE_BUSY_TIMEOUT=1000"));
64  if (!connection.open()) {
65  qWarning() << "AsyncQuery::initdb: Error opening state database. Window positions will not be saved or restored." << m_dbName << connection.lastError().driverText() << connection.lastError().databaseText();
66  return false;
67  }
68  QSqlQuery query(connection);
69 
70  if (!connection.tables().contains(QStringLiteral("geometry"))) {
71  QString geometryQuery = QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);");
72  if (!query.exec(geometryQuery)) {
73  logSqlError(query);
74  return false;
75  }
76  }
77 
78  if (!connection.tables().contains(QStringLiteral("state"))) {
79  QString stateQuery = QStringLiteral("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);");
80  if (!query.exec(stateQuery)) {
81  logSqlError(query);
82  return false;
83  }
84  }
85 
86  if (!connection.tables().contains(QStringLiteral("stage"))) {
87  QString stageQuery = QStringLiteral("CREATE TABLE stage(appId TEXT UNIQUE, stage INTEGER);");
88  if (!query.exec(stageQuery)) {
89  logSqlError(query);
90  return false;
91  }
92  }
93  m_ok = true;
94  return true;
95  }
96 
97  Q_INVOKABLE int getState(const QString &windowId) const
98  {
99  if (!m_ok) {
100  return -1;
101  }
102  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
103  QSqlQuery query(connection);
104  query.prepare(m_getStateQuery);
105  query.bindValue(":windowId", windowId);
106  query.exec();
107  if (!query.isActive() || !query.isSelect()) {
108  logSqlError(query);
109  return -1;
110  }
111  if (!query.first()) {
112  return -1;
113  }
114  bool converted = false;
115  QVariant resultStr = query.value(0);
116  int result = resultStr.toInt(&converted);
117  if (converted) {
118  return result;
119  } else {
120  qWarning() << "getState result expected integer, got " << resultStr;
121  return -1;
122  }
123  }
124 
125  Q_INVOKABLE QRect getGeometry(const QString &windowId) const
126  {
127  if (!m_ok) {
128  return QRect();
129  }
130  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
131  QSqlQuery query(connection);
132  query.prepare(m_getGeometryQuery);
133  query.bindValue(":windowId", windowId);
134  query.exec();
135  if (!query.isActive() || !query.isSelect()) {
136  logSqlError(query);
137  return QRect();
138  }
139 
140  if (!query.first()) {
141  return QRect();
142  }
143 
144  bool xConverted, yConverted, widthConverted, heightConverted = false;
145  int x, y, width, height;
146  QVariant xResultStr = query.value(QStringLiteral("x"));
147  QVariant yResultStr = query.value(QStringLiteral("y"));
148  QVariant widthResultStr = query.value(QStringLiteral("width"));
149  QVariant heightResultStr = query.value(QStringLiteral("height"));
150  x = xResultStr.toInt(&xConverted);
151  y = yResultStr.toInt(&yConverted);
152  width = widthResultStr.toInt(&widthConverted);
153  height = heightResultStr.toInt(&heightConverted);
154 
155  if (xConverted && yConverted && widthConverted && heightConverted) {
156  return QRect(x, y, width, height);
157  } else {
158  qWarning() << "getGeometry result expected integers, got x:"
159  << xResultStr << "y:" << yResultStr << "width" << widthResultStr
160  << "height:" << heightResultStr;
161  return QRect();
162  }
163 
164  }
165 
166  Q_INVOKABLE int getStage(const QString &appId) const
167  {
168  if (!m_ok) {
169  return -1;
170  }
171  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
172  QSqlQuery query(connection);
173  query.prepare(m_getStageQuery);
174  query.bindValue(":appId", appId);
175  query.exec();
176  if (!query.isActive() || !query.isSelect()) {
177  logSqlError(query);
178  return -1;
179  }
180  if (!query.first()) {
181  return -1;
182  }
183  bool converted = false;
184  QVariant resultStr = query.value(0);
185  int result = resultStr.toInt(&converted);
186  if (converted) {
187  return result;
188  } else {
189  qWarning() << "getStage result expected integer, got " << resultStr;
190  return -1;
191  }
192  }
193 
194 public Q_SLOTS:
195 
196  void saveState(const QString &windowId, WindowStateStorage::WindowState state)
197  {
198  if (!m_ok) {
199  return;
200  }
201  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
202  QSqlQuery query(connection);
203  query.prepare(m_saveStateQuery);
204  query.bindValue(":windowId", windowId);
205  query.bindValue(":state", (int)state);
206  if (!query.exec()) {
207  logSqlError(query);
208  }
209  }
210 
211  void saveGeometry(const QString &windowId, const QRect &rect)
212  {
213  if (!m_ok) {
214  return;
215  }
216  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
217  QSqlQuery query(connection);
218  query.prepare(m_saveGeometryQuery);
219  query.bindValue(":windowId", windowId);
220  query.bindValue(":x", rect.x());
221  query.bindValue(":y", rect.y());
222  query.bindValue(":width", rect.width());
223  query.bindValue(":height", rect.height());
224  if (!query.exec()) {
225  logSqlError(query);
226  }
227  }
228 
229  void saveStage(const QString &appId, int stage)
230  {
231  if (!m_ok) {
232  return;
233  }
234  QSqlDatabase connection = QSqlDatabase::database(m_connectionName);
235  QSqlQuery query(connection);
236  query.prepare(m_saveStageQuery);
237  query.bindValue(":appId", appId);
238  query.bindValue(":stage", stage);
239  if (!query.exec()) {
240  logSqlError(query);
241  }
242  }
243 
244 private:
245  static const QString m_connectionName;
246  static const QString m_getStateQuery;
247  static const QString m_saveStateQuery;
248  static const QString m_getGeometryQuery;
249  static const QString m_saveGeometryQuery;
250  static const QString m_getStageQuery;
251  static const QString m_saveStageQuery;
252 
253  static void logSqlError(const QSqlQuery query)
254  {
255  qWarning() << "Error executing query" << query.lastQuery()
256  << "Driver error:" << query.lastError().driverText()
257  << "Database error:" << query.lastError().databaseText();
258  }
259 
260  QString m_dbName;
261  bool m_ok = false;
262 };
263 
264 const QString AsyncQuery::m_connectionName = QStringLiteral("WindowStateStorage");
265 const QString AsyncQuery::m_getStateQuery = QStringLiteral("SELECT state FROM state WHERE windowId = :windowId");
266 const QString AsyncQuery::m_saveStateQuery = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values (:windowId, :state)");
267 const QString AsyncQuery::m_getGeometryQuery = QStringLiteral("SELECT * FROM geometry WHERE windowId = :windowId");
268 const QString AsyncQuery::m_saveGeometryQuery = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values (:windowId, :x, :y, :width, :height)");
269 const QString AsyncQuery::m_getStageQuery = QStringLiteral("SELECT stage FROM stage WHERE appId = :appId");
270 const QString AsyncQuery::m_saveStageQuery = QStringLiteral("INSERT OR REPLACE INTO stage (appId, stage) values (:appId, :stage)");
271 
272 WindowStateStorage::WindowStateStorage(const QString &dbName, QObject *parent):
273  QObject(parent),
274  m_thread()
275 {
276  qRegisterMetaType<WindowStateStorage::WindowState>("WindowStateStorage::WindowState");
277  QString dbFile;
278  if (dbName.isEmpty()) {
279  const QString dbPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/unity8/");
280  QDir dir;
281  dir.mkpath(dbPath);
282  dbFile = QString(dbPath) + QStringLiteral("windowstatestorage.sqlite");
283  } else {
284  dbFile = dbName;
285  }
286  m_asyncQuery = new AsyncQuery(dbFile);
287  m_asyncQuery->moveToThread(&m_thread);
288  connect(&m_thread, &QThread::finished, m_asyncQuery, &QObject::deleteLater);
289  m_thread.start();
290  // Note that we're relying on initdb being called before any other methods
291  // on AsyncQuery. Given the current behavior of QueuedConnection (slots
292  // invoked in the order they are received), this is fine.
293  QMetaObject::invokeMethod(m_asyncQuery, "initdb",
294  Qt::QueuedConnection);
295  connect(this, &WindowStateStorage::saveState, m_asyncQuery, &AsyncQuery::saveState);
296  connect(this, &WindowStateStorage::saveGeometry, m_asyncQuery, &AsyncQuery::saveGeometry);
297  connect(this, &WindowStateStorage::saveStage, m_asyncQuery, &AsyncQuery::saveStage);
298 }
299 
300 WindowStateStorage::~WindowStateStorage()
301 {
302  m_thread.quit();
303  m_thread.wait();
304 }
305 
306 WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
307 {
308  int state;
309 
310  QMetaObject::invokeMethod(m_asyncQuery, "getState", Qt::BlockingQueuedConnection,
311  Q_RETURN_ARG(int, state),
312  Q_ARG(const QString&, windowId)
313  );
314 
315  if (state == -1) {
316  return defaultValue;
317  }
318 
319  return (WindowState)state;
320 }
321 
322 int WindowStateStorage::getStage(const QString &appId, int defaultValue) const
323 {
324  int stage;
325 
326  QMetaObject::invokeMethod(m_asyncQuery, "getStage", Qt::BlockingQueuedConnection,
327  Q_RETURN_ARG(int, stage),
328  Q_ARG(const QString&, appId)
329  );
330 
331  if (stage == -1) {
332  return defaultValue;
333  }
334 
335  return stage;
336 }
337 
338 QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
339 {
340  QRect geometry;
341  QMetaObject::invokeMethod(m_asyncQuery, "getGeometry", Qt::BlockingQueuedConnection,
342  Q_RETURN_ARG(QRect, geometry),
343  Q_ARG(const QString&, windowId)
344  );
345  if (geometry.isNull() || !geometry.isValid()) {
346  return defaultValue;
347  }
348  return geometry;
349 }
350 
351 const QString WindowStateStorage::getDbName()
352 {
353  QString dbName;
354  QMetaObject::invokeMethod(m_asyncQuery, "getDbName", Qt::BlockingQueuedConnection,
355  Q_RETURN_ARG(QString, dbName)
356  );
357  return QString(dbName);
358 }
359 
360 Mir::State WindowStateStorage::toMirState(WindowState state) const
361 {
362  // assumes a single state (not an OR of several)
363  switch (state) {
364  case WindowStateMaximized: return Mir::MaximizedState;
365  case WindowStateMinimized: return Mir::MinimizedState;
366  case WindowStateFullscreen: return Mir::FullscreenState;
367  case WindowStateMaximizedLeft: return Mir::MaximizedLeftState;
368  case WindowStateMaximizedRight: return Mir::MaximizedRightState;
369  case WindowStateMaximizedHorizontally: return Mir::HorizMaximizedState;
370  case WindowStateMaximizedVertically: return Mir::VertMaximizedState;
371  case WindowStateMaximizedTopLeft: return Mir::MaximizedTopLeftState;
372  case WindowStateMaximizedTopRight: return Mir::MaximizedTopRightState;
373  case WindowStateMaximizedBottomLeft: return Mir::MaximizedBottomLeftState;
374  case WindowStateMaximizedBottomRight: return Mir::MaximizedBottomRightState;
375 
376  case WindowStateNormal:
377  case WindowStateRestored:
378  default:
379  return Mir::RestoredState;
380  }
381 }
382 
383 #include "windowstatestorage.moc"