Force portrait orientation on connected devices, remove x100 heuristic from transaction matching, and add secondary text search filter in monitoring app.
This commit is contained in:
parent
e8ddde7e36
commit
2c2d58021f
@ -816,6 +816,33 @@ QString AdbUtils::getForegroundPackage(const QString &deviceId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
bool AdbUtils::ensurePortraitOrientation(const QString &deviceId) {
|
||||
if (deviceId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QString adb = adbPath();
|
||||
QString out;
|
||||
if (!startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "input"}, &out)) {
|
||||
return false; // adb недоступен — пропускаем (best-effort)
|
||||
}
|
||||
|
||||
// SurfaceOrientation: 0=0°(portrait) 1=90°(landscape) 2=180° 3=270°(landscape)
|
||||
static const QRegularExpression re(R"(SurfaceOrientation:\s*(\d))");
|
||||
const QRegularExpressionMatch m = re.match(out);
|
||||
const int orient = m.hasMatch() ? m.captured(1).toInt() : 0;
|
||||
|
||||
if (orient == 1 || orient == 3) {
|
||||
qInfo() << "[AdbUtils] device" << deviceId
|
||||
<< "in landscape (orient" << orient << ") → forcing portrait";
|
||||
// Автоповорот выключаем, иначе сенсор вернёт landscape.
|
||||
startProcess(adb, {"-s", deviceId, "shell", "settings", "put",
|
||||
"system", "accelerometer_rotation", "0"});
|
||||
startProcess(adb, {"-s", deviceId, "shell", "settings", "put",
|
||||
"system", "user_rotation", "0"});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) {
|
||||
const QString adb = adbPath();
|
||||
|
||||
|
||||
@ -62,6 +62,13 @@ public:
|
||||
// Возвращает пакет приложения на переднем плане (foreground), либо пустую строку при ошибке
|
||||
static QString getForegroundPackage(const QString &deviceId);
|
||||
|
||||
// Проверяет реальную ориентацию экрана (dumpsys input → SurfaceOrientation).
|
||||
// Если экран в горизонтальной ориентации (landscape, 90°/270°), принудительно
|
||||
// ставит вертикальную: выключает автоповорот и выставляет user_rotation=0.
|
||||
// Best-effort: при ошибке adb просто ничего не делает. Возвращает true, если
|
||||
// экран в портрете (изначально или после форсирования).
|
||||
static bool ensurePortraitOrientation(const QString &deviceId);
|
||||
|
||||
private:
|
||||
static QString adbDir();
|
||||
static bool startProcess(const QString &program, const QStringList &args,
|
||||
|
||||
@ -719,14 +719,16 @@ void GetLastTransactionsScript::searchAll(
|
||||
const double absListAmt = qAbs(tx.amount);
|
||||
|
||||
// Выбираем подходящую цель: совпадение по сумме + пред-фильтры даты/времени.
|
||||
// Список Выписки, экран деталей и поисковый таргет — все в мажорных
|
||||
// единицах (TJS, 2 знака), поэтому сверяем сумму напрямую. Эвристик
|
||||
// "x100" убран: он ложно матчил реальную транзакцию на 1.00 к поиску
|
||||
// на 100.00 (1.00*100==100.00) и затем домножал результат до 10000.
|
||||
int matchIdx = -1;
|
||||
bool matchedX100 = false;
|
||||
for (int ti = 0; ti < pending.size(); ++ti) {
|
||||
const double searchAbs = qAbs(pending[ti].first);
|
||||
const QDateTime &cand = pending[ti].second;
|
||||
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
|
||||
const bool amountX100 = qAbs(absListAmt * 100.0 - searchAbs) < 0.01;
|
||||
if (!amountDirect && !amountX100) continue;
|
||||
if (!amountDirect) continue;
|
||||
|
||||
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
|
||||
// DCWallet транзакции получают дату из groupDates ImageView, которая
|
||||
@ -753,14 +755,12 @@ void GetLastTransactionsScript::searchAll(
|
||||
}
|
||||
|
||||
matchIdx = ti;
|
||||
matchedX100 = amountX100 && !amountDirect;
|
||||
break;
|
||||
}
|
||||
if (matchIdx < 0) continue;
|
||||
const QDateTime searchTime = pending[matchIdx].second;
|
||||
|
||||
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
|
||||
<< (matchedX100 ? "(scaled x100)" : "(direct)")
|
||||
<< "date:" << tx.date << "time:" << tx.time
|
||||
<< "→ target idx" << matchIdx;
|
||||
|
||||
@ -947,8 +947,8 @@ void GetLastTransactionsScript::searchAll(
|
||||
// Формируем task_result напрямую из распарсенных данных и отдаём
|
||||
// EventHandler через сигнал — без записи/чтения БД.
|
||||
{
|
||||
const double finalAmount = matchedX100 ? detail.amount * 100.0 : detail.amount;
|
||||
const double finalFee = matchedX100 ? detail.fee * 100.0 : detail.fee;
|
||||
const double finalAmount = detail.amount;
|
||||
const double finalFee = detail.fee;
|
||||
|
||||
QDateTime txTime = QDateTime::fromString(
|
||||
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
||||
|
||||
@ -982,7 +982,7 @@ def admin_logout() -> Response:
|
||||
return resp
|
||||
|
||||
|
||||
def _query_entries(kind, type_, event, q, date_from, date_to, page):
|
||||
def _query_entries(kind, type_, event, q, date_from, date_to, page, q2=""):
|
||||
where, params = [], []
|
||||
if kind:
|
||||
where.append("kind=?"); params.append(kind)
|
||||
@ -992,6 +992,8 @@ def _query_entries(kind, type_, event, q, date_from, date_to, page):
|
||||
where.append("event LIKE ?"); params.append(f"%{event}%")
|
||||
if q:
|
||||
where.append("message LIKE ?"); params.append(f"%{q}%")
|
||||
if q2:
|
||||
where.append("message LIKE ?"); params.append(f"%{q2}%")
|
||||
df = _parse_date(date_from)
|
||||
if df is not None:
|
||||
where.append("ts>=?"); params.append(df)
|
||||
@ -1096,6 +1098,7 @@ def admin_dashboard(
|
||||
type: str = "",
|
||||
event: str = "",
|
||||
q: str = "",
|
||||
q2: str = "",
|
||||
date_from: str = "",
|
||||
date_to: str = "",
|
||||
page: int = 0,
|
||||
@ -1103,11 +1106,11 @@ def admin_dashboard(
|
||||
if not _is_authed(request):
|
||||
return RedirectResponse("/admin/login", status_code=303)
|
||||
|
||||
rows, total, types = _query_entries(kind, type, event, q, date_from, date_to, page)
|
||||
rows, total, types = _query_entries(kind, type, event, q, date_from, date_to, page, q2)
|
||||
page = max(0, page)
|
||||
|
||||
current_url = (f"/admin?kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
|
||||
f"&q={html_lib.escape(q)}&date_from={date_from}&date_to={date_to}&page={page}")
|
||||
f"&q={html_lib.escape(q)}&q2={html_lib.escape(q2)}&date_from={date_from}&date_to={date_to}&page={page}")
|
||||
|
||||
trs = _build_entry_trs(rows, current_url)
|
||||
|
||||
@ -1116,7 +1119,7 @@ def admin_dashboard(
|
||||
|
||||
def link(p: int) -> str:
|
||||
qs = (f"kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
|
||||
f"&q={html_lib.escape(q)}&date_from={date_from}&date_to={date_to}&page={p}")
|
||||
f"&q={html_lib.escape(q)}&q2={html_lib.escape(q2)}&date_from={date_from}&date_to={date_to}&page={p}")
|
||||
return f"/admin?{qs}"
|
||||
|
||||
shown_from = page * PAGE_SIZE + 1 if total else 0
|
||||
@ -1133,6 +1136,7 @@ def admin_dashboard(
|
||||
<label>Тип<select name=type>{type_opts}</select></label>
|
||||
<label>Event<input name=event value="{html_lib.escape(event)}" placeholder="task_error…"></label>
|
||||
<label>Поиск в тексте<input name=q value="{html_lib.escape(q)}" placeholder="подстрока"></label>
|
||||
<label>И ещё<input name=q2 value="{html_lib.escape(q2)}" placeholder="вторая подстрока"></label>
|
||||
<label>С (UTC)<input name=date_from type=datetime-local step=1 value="{html_lib.escape(date_from)}"></label>
|
||||
<label>По (UTC)<input name=date_to type=datetime-local step=1 value="{html_lib.escape(date_to)}"></label>
|
||||
<button type=submit>Фильтр</button>
|
||||
@ -1219,6 +1223,7 @@ def admin_entries_fragment(
|
||||
type: str = "",
|
||||
event: str = "",
|
||||
q: str = "",
|
||||
q2: str = "",
|
||||
date_from: str = "",
|
||||
date_to: str = "",
|
||||
page: int = 0,
|
||||
@ -1227,8 +1232,8 @@ def admin_entries_fragment(
|
||||
raise HTTPException(status_code=401, detail="auth required")
|
||||
page = max(0, page)
|
||||
current_url = (f"/admin?kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
|
||||
f"&q={html_lib.escape(q)}&date_from={date_from}&date_to={date_to}&page={page}")
|
||||
rows, total, _ = _query_entries(kind, type, event, q, date_from, date_to, page)
|
||||
f"&q={html_lib.escape(q)}&q2={html_lib.escape(q2)}&date_from={date_from}&date_to={date_to}&page={page}")
|
||||
rows, total, _ = _query_entries(kind, type, event, q, date_from, date_to, page, q2)
|
||||
trs = _build_entry_trs(rows, current_url)
|
||||
return JSONResponse({"rows_html": "".join(trs), "total": total})
|
||||
|
||||
|
||||
@ -719,6 +719,20 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
|
||||
const auto thr = new QThread(this);
|
||||
const QString key = account.deviceId;
|
||||
|
||||
// Перед запуском любой задачи гарантируем вертикальную ориентацию экрана:
|
||||
// если телефон в landscape — принудительно ставим portrait. Выполняется в
|
||||
// рабочем потоке (functor без context → исполняется в потоке-эмиттере thr),
|
||||
// поэтому adb-I/O не блокирует поток диспетчера. Коннект добавлен до setup(),
|
||||
// значит сработает раньше, чем worker::start → doStart().
|
||||
const QString canonicalDeviceId = account.deviceId;
|
||||
connect(thr, &QThread::started, [canonicalDeviceId]() {
|
||||
DeviceInfo dev = DeviceDAO::getDeviceById(canonicalDeviceId);
|
||||
if (dev.id.isEmpty()) dev = DeviceDAO::findByAdbSerial(canonicalDeviceId);
|
||||
const QString serial = dev.adbSerial.isEmpty() ? canonicalDeviceId
|
||||
: dev.adbSerial;
|
||||
AdbUtils::ensurePortraitOrientation(serial);
|
||||
});
|
||||
|
||||
// Generic helper: подключает сигналы и переносит worker в поток
|
||||
auto setup = [&](auto *worker) {
|
||||
using W = std::remove_pointer_t<decltype(worker)>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user