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 {};
|
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) {
|
void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) {
|
||||||
const QString adb = adbPath();
|
const QString adb = adbPath();
|
||||||
|
|
||||||
|
|||||||
@ -62,6 +62,13 @@ public:
|
|||||||
// Возвращает пакет приложения на переднем плане (foreground), либо пустую строку при ошибке
|
// Возвращает пакет приложения на переднем плане (foreground), либо пустую строку при ошибке
|
||||||
static QString getForegroundPackage(const QString &deviceId);
|
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:
|
private:
|
||||||
static QString adbDir();
|
static QString adbDir();
|
||||||
static bool startProcess(const QString &program, const QStringList &args,
|
static bool startProcess(const QString &program, const QStringList &args,
|
||||||
|
|||||||
@ -719,14 +719,16 @@ void GetLastTransactionsScript::searchAll(
|
|||||||
const double absListAmt = qAbs(tx.amount);
|
const double absListAmt = qAbs(tx.amount);
|
||||||
|
|
||||||
// Выбираем подходящую цель: совпадение по сумме + пред-фильтры даты/времени.
|
// Выбираем подходящую цель: совпадение по сумме + пред-фильтры даты/времени.
|
||||||
|
// Список Выписки, экран деталей и поисковый таргет — все в мажорных
|
||||||
|
// единицах (TJS, 2 знака), поэтому сверяем сумму напрямую. Эвристик
|
||||||
|
// "x100" убран: он ложно матчил реальную транзакцию на 1.00 к поиску
|
||||||
|
// на 100.00 (1.00*100==100.00) и затем домножал результат до 10000.
|
||||||
int matchIdx = -1;
|
int matchIdx = -1;
|
||||||
bool matchedX100 = false;
|
|
||||||
for (int ti = 0; ti < pending.size(); ++ti) {
|
for (int ti = 0; ti < pending.size(); ++ti) {
|
||||||
const double searchAbs = qAbs(pending[ti].first);
|
const double searchAbs = qAbs(pending[ti].first);
|
||||||
const QDateTime &cand = pending[ti].second;
|
const QDateTime &cand = pending[ti].second;
|
||||||
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
|
const bool amountDirect = qAbs(absListAmt - searchAbs) < 0.01;
|
||||||
const bool amountX100 = qAbs(absListAmt * 100.0 - searchAbs) < 0.01;
|
if (!amountDirect) continue;
|
||||||
if (!amountDirect && !amountX100) continue;
|
|
||||||
|
|
||||||
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
|
// Pre-filter по ДАТЕ: только если дата надёжная (из content-desc P2P формата).
|
||||||
// DCWallet транзакции получают дату из groupDates ImageView, которая
|
// DCWallet транзакции получают дату из groupDates ImageView, которая
|
||||||
@ -753,14 +755,12 @@ void GetLastTransactionsScript::searchAll(
|
|||||||
}
|
}
|
||||||
|
|
||||||
matchIdx = ti;
|
matchIdx = ti;
|
||||||
matchedX100 = amountX100 && !amountDirect;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (matchIdx < 0) continue;
|
if (matchIdx < 0) continue;
|
||||||
const QDateTime searchTime = pending[matchIdx].second;
|
const QDateTime searchTime = pending[matchIdx].second;
|
||||||
|
|
||||||
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
|
qDebug() << "[Dushanbe::GetLastTransactions] Amount match:" << tx.amount
|
||||||
<< (matchedX100 ? "(scaled x100)" : "(direct)")
|
|
||||||
<< "date:" << tx.date << "time:" << tx.time
|
<< "date:" << tx.date << "time:" << tx.time
|
||||||
<< "→ target idx" << matchIdx;
|
<< "→ target idx" << matchIdx;
|
||||||
|
|
||||||
@ -947,8 +947,8 @@ void GetLastTransactionsScript::searchAll(
|
|||||||
// Формируем task_result напрямую из распарсенных данных и отдаём
|
// Формируем task_result напрямую из распарсенных данных и отдаём
|
||||||
// EventHandler через сигнал — без записи/чтения БД.
|
// EventHandler через сигнал — без записи/чтения БД.
|
||||||
{
|
{
|
||||||
const double finalAmount = matchedX100 ? detail.amount * 100.0 : detail.amount;
|
const double finalAmount = detail.amount;
|
||||||
const double finalFee = matchedX100 ? detail.fee * 100.0 : detail.fee;
|
const double finalFee = detail.fee;
|
||||||
|
|
||||||
QDateTime txTime = QDateTime::fromString(
|
QDateTime txTime = QDateTime::fromString(
|
||||||
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");
|
||||||
|
|||||||
@ -982,7 +982,7 @@ def admin_logout() -> Response:
|
|||||||
return resp
|
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 = [], []
|
where, params = [], []
|
||||||
if kind:
|
if kind:
|
||||||
where.append("kind=?"); params.append(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}%")
|
where.append("event LIKE ?"); params.append(f"%{event}%")
|
||||||
if q:
|
if q:
|
||||||
where.append("message LIKE ?"); params.append(f"%{q}%")
|
where.append("message LIKE ?"); params.append(f"%{q}%")
|
||||||
|
if q2:
|
||||||
|
where.append("message LIKE ?"); params.append(f"%{q2}%")
|
||||||
df = _parse_date(date_from)
|
df = _parse_date(date_from)
|
||||||
if df is not None:
|
if df is not None:
|
||||||
where.append("ts>=?"); params.append(df)
|
where.append("ts>=?"); params.append(df)
|
||||||
@ -1096,6 +1098,7 @@ def admin_dashboard(
|
|||||||
type: str = "",
|
type: str = "",
|
||||||
event: str = "",
|
event: str = "",
|
||||||
q: str = "",
|
q: str = "",
|
||||||
|
q2: str = "",
|
||||||
date_from: str = "",
|
date_from: str = "",
|
||||||
date_to: str = "",
|
date_to: str = "",
|
||||||
page: int = 0,
|
page: int = 0,
|
||||||
@ -1103,11 +1106,11 @@ def admin_dashboard(
|
|||||||
if not _is_authed(request):
|
if not _is_authed(request):
|
||||||
return RedirectResponse("/admin/login", status_code=303)
|
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)
|
page = max(0, page)
|
||||||
|
|
||||||
current_url = (f"/admin?kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
|
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)
|
trs = _build_entry_trs(rows, current_url)
|
||||||
|
|
||||||
@ -1116,7 +1119,7 @@ def admin_dashboard(
|
|||||||
|
|
||||||
def link(p: int) -> str:
|
def link(p: int) -> str:
|
||||||
qs = (f"kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
|
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}"
|
return f"/admin?{qs}"
|
||||||
|
|
||||||
shown_from = page * PAGE_SIZE + 1 if total else 0
|
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>Тип<select name=type>{type_opts}</select></label>
|
||||||
<label>Event<input name=event value="{html_lib.escape(event)}" placeholder="task_error…"></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=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_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>
|
<label>По (UTC)<input name=date_to type=datetime-local step=1 value="{html_lib.escape(date_to)}"></label>
|
||||||
<button type=submit>Фильтр</button>
|
<button type=submit>Фильтр</button>
|
||||||
@ -1219,6 +1223,7 @@ def admin_entries_fragment(
|
|||||||
type: str = "",
|
type: str = "",
|
||||||
event: str = "",
|
event: str = "",
|
||||||
q: str = "",
|
q: str = "",
|
||||||
|
q2: str = "",
|
||||||
date_from: str = "",
|
date_from: str = "",
|
||||||
date_to: str = "",
|
date_to: str = "",
|
||||||
page: int = 0,
|
page: int = 0,
|
||||||
@ -1227,8 +1232,8 @@ def admin_entries_fragment(
|
|||||||
raise HTTPException(status_code=401, detail="auth required")
|
raise HTTPException(status_code=401, detail="auth required")
|
||||||
page = max(0, page)
|
page = max(0, page)
|
||||||
current_url = (f"/admin?kind={kind}&type={html_lib.escape(type)}&event={html_lib.escape(event)}"
|
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}")
|
||||||
rows, total, _ = _query_entries(kind, type, event, q, date_from, date_to, page)
|
rows, total, _ = _query_entries(kind, type, event, q, date_from, date_to, page, q2)
|
||||||
trs = _build_entry_trs(rows, current_url)
|
trs = _build_entry_trs(rows, current_url)
|
||||||
return JSONResponse({"rows_html": "".join(trs), "total": total})
|
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 auto thr = new QThread(this);
|
||||||
const QString key = account.deviceId;
|
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 в поток
|
// Generic helper: подключает сигналы и переносит worker в поток
|
||||||
auto setup = [&](auto *worker) {
|
auto setup = [&](auto *worker) {
|
||||||
using W = std::remove_pointer_t<decltype(worker)>;
|
using W = std::remove_pointer_t<decltype(worker)>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user