Browse Source

Звонки: надёжность отмены (INVITE-кодограмма + watchdog) и вибрация входящего по ringer-режиму

proxy
evgeny 1 week ago
parent
commit
cc269e810c
  1. 11
      doc/chat_call_arch.md
  2. 12
      doc/tasks.md
  3. 62
      src/call/call.c
  4. 7
      src/call/call_proto.h
  5. 123
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/RingtonePlayer.kt

11
doc/chat_call_arch.md

@ -63,10 +63,19 @@ Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`. NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
Таймауты: Таймауты:
- ring-timeout 45с (нет ответа → CANCEL); - **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
держит маршрут живым;
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
- ring-timeout 45с (вызывающий, нет ответа → CANCEL);
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail; - connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP. - **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Завершающие кодограммы (HANGUP/CANCEL/DECLINE) шлются без ретрансмиссий — потеря
компенсируется watchdog'ом 2с у вызываемого. Запоздалые кодограммы вызова при
идущем разговоре (`CONNECTING`/`ACTIVE`) игнорируются.
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event). Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`; Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;

12
doc/tasks.md

@ -77,10 +77,20 @@
Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing` Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing`
(MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error; (MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error;
добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок
зависали навсегда. Android: `CallRingback` (`ToneGenerator` DIAL/RINGTONE), зависали навсегда. Android: `CallRingback` (MediaPlayer loop из res/raw),
подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner. подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner.
Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing). Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).
[+] **Надёжность отмены вызова (кодограмма + watchdog)** — сделано. Отмена вызова
(HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch,
прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в
RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»:
- вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING);
- у вызываемого watchdog 2с — нет кодограммы → вызов сорван (`CALL_REASON_CANCEL`);
- завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog);
- запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются.
`CALL_INVITE_RETRY_TB=3000`, `CALL_RING_DEAD_TB=20000`, `CALL_NO_ROUTE_MAX_STRIKES=20`.
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e) ## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`): Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`):

62
src/call/call.c

@ -56,8 +56,8 @@ struct call_session {
struct call_ctx* ctx; /* обратная ссылка (для коллбэков conn_mgr/таймеров) */ struct call_ctx* ctx; /* обратная ссылка (для коллбэков conn_mgr/таймеров) */
struct CONN_MGR_HANDLE* cm_handle; struct CONN_MGR_HANDLE* cm_handle;
struct ETCP_CONN* data_conn; /* прямой data-коннект звонка (NULL для INDIRECT) */ struct ETCP_CONN* data_conn; /* прямой data-коннект звонка (NULL для INDIRECT) */
void* ring_timer; /* caller: 45с ожидания ответа */ void* ring_timer; /* caller: 45с ожидания ответа; callee: 2с watchdog кодограмм */
void* invite_timer; /* caller: периодический ретрансмит INVITE (до RINGING) */ void* invite_timer; /* caller: кодограмма INVITE каждые 300мс (OUTGOING+RINGING) */
uint8_t no_route_strikes; /* caller: подряд ретрансмитов без маршрута (fail-fast) */ uint8_t no_route_strikes; /* caller: подряд ретрансмитов без маршрута (fail-fast) */
void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */ void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */
void* teardown_timer; /* отложенный разрыв conn_mgr (500мс после завершения) */ void* teardown_timer; /* отложенный разрыв conn_mgr (500мс после завершения) */
@ -382,22 +382,24 @@ static void call_ring_timeout_cb(void* arg) {
call_end(ctx, s, CALL_REASON_RING_TIMEOUT); call_end(ctx, s, CALL_REASON_RING_TIMEOUT);
} }
/* Периодический ретрансмит INVITE (caller, до RINGING). Покрывает потерю INVITE/RINGING /* Периодическая кодограмма INVITE (caller) каждые 300мс на всём протяжении вызова
* и появление маршрута позже. При стойком отсутствии маршрута — fail-fast CONNECT_FAIL. */ * (OUTGOING + RINGING) — «сердцебиение»: покрывает потерю INVITE/RINGING, держит
* маршрут и кормит watchdog вызываемого (2с без кодограммы → вызов сорван).
* При стойком отсутствии маршрута — fail-fast CONNECT_FAIL. */
static void call_invite_retry_cb(void* arg) { static void call_invite_retry_cb(void* arg) {
struct call_session* s = (struct call_session*)arg; struct call_session* s = (struct call_session*)arg;
if (!s || !s->ctx) return; if (!s || !s->ctx) return;
struct call_ctx* ctx = s->ctx; struct call_ctx* ctx = s->ctx;
s->invite_timer = NULL; s->invite_timer = NULL;
if (s->state == CALL_ST_ENDED) return; if (s->state == CALL_ST_ENDED) return;
if (s->state != CALL_ST_OUTGOING) return; /* RINGING — INVITE доставлен, ретрансмит не нужен */ if (s->state != CALL_ST_OUTGOING && s->state != CALL_ST_RINGING) return; /* CONNECTING/ACTIVE — звонок идёт */
if (etcp_router_has_route(ctx->inst, s->group_id, s->peer_node_id)) { if (etcp_router_has_route(ctx->inst, s->group_id, s->peer_node_id)) {
s->no_route_strikes = 0; s->no_route_strikes = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE retry id=%016llx (route ok)", CALL_ID, (unsigned long long)s->call_id); DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx (route ok)", CALL_ID, (unsigned long long)s->call_id);
call_send_invite(ctx, s); call_send_invite(ctx, s);
} else if (++s->no_route_strikes >= CALL_NO_ROUTE_MAX_STRIKES) { } else if (++s->no_route_strikes >= CALL_NO_ROUTE_MAX_STRIKES) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: no route after %d retries id=%016llx -> CONNECT_FAIL", DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: no route after %d strikes id=%016llx -> CONNECT_FAIL",
CALL_ID, s->no_route_strikes, (unsigned long long)s->call_id); CALL_ID, s->no_route_strikes, (unsigned long long)s->call_id);
uint8_t buf[9]; uint8_t buf[9];
memcpy(buf, &s->call_id, 8); memcpy(buf, &s->call_id, 8);
@ -406,13 +408,26 @@ static void call_invite_retry_cb(void* arg) {
call_end(ctx, s, CALL_REASON_CONNECT_FAIL); call_end(ctx, s, CALL_REASON_CONNECT_FAIL);
return; return;
} else { } else {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE retry id=%016llx no route (strike %d/%d)", DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx no route (strike %d/%d)",
CALL_ID, (unsigned long long)s->call_id, s->no_route_strikes, CALL_NO_ROUTE_MAX_STRIKES); CALL_ID, (unsigned long long)s->call_id, s->no_route_strikes, CALL_NO_ROUTE_MAX_STRIKES);
} }
s->invite_timer = uasync_set_timeout(ctx->inst->ua, CALL_INVITE_RETRY_TB, s, call_invite_retry_cb, "call_invite_retry"); s->invite_timer = uasync_set_timeout(ctx->inst->ua, CALL_INVITE_RETRY_TB, s, call_invite_retry_cb, "call_invite_retry");
} }
/* Вызываемый: 2с без кодограммы INVITE → вызывающий пропал (отмена потерялась) — закрываем. */
static void call_ring_dead_cb(void* arg) {
struct call_session* s = (struct call_session*)arg;
if (!s || !s->ctx) return;
struct call_ctx* ctx = s->ctx;
s->ring_timer = NULL;
if (s->state != CALL_ST_RINGING) return;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: ring dead (2с без кодограммы) id=%016llx -> end",
CALL_ID, (unsigned long long)s->call_id);
call_end(ctx, s, CALL_REASON_CANCEL);
}
static void call_traffic_timeout_cb(void* arg) { static void call_traffic_timeout_cb(void* arg) {
struct call_session* s = (struct call_session*)arg; struct call_session* s = (struct call_session*)arg;
if (!s || !s->ctx) return; if (!s || !s->ctx) return;
@ -513,15 +528,23 @@ static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t gro
memcpy(&call_id, b + 1, 8); memcpy(&call_id, b + 1, 8);
memcpy(&inv_group, b + 9, 8); memcpy(&inv_group, b + 9, 8);
/* повторная доставка INVITE (тот же call_id) — идемпотентно RINGING */ /* кодограмма-дубликат (тот же call_id): пока звоним — пере-взводим watchdog и
* идемпотентно отвечаем RINGING; в разговоре (CONNECTING/ACTIVE) — игнорируем. */
struct call_session* s = call_session_find(ctx, call_id); struct call_session* s = call_session_find(ctx, call_id);
if (s && s->state != CALL_ST_ENDED) { if (s) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE retransmit id=%016llx -> re-RINGING", CALL_ID, (unsigned long long)call_id); if (s->role == CALL_ROLE_CALLEE && s->state == CALL_ST_RINGING) {
uint8_t pkt[sizeof(struct call_pkt_ringing)]; if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; }
struct call_pkt_ringing* r = (struct call_pkt_ringing*)pkt; s->ring_timer = uasync_set_timeout(ctx->inst->ua, CALL_RING_DEAD_TB, s, call_ring_dead_cb, "call_ring_dead");
r->subcmd = CALL_SUBCMD_RINGING; DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx -> re-RINGING + re-arm dead", CALL_ID, (unsigned long long)call_id);
r->call_id = call_id; uint8_t pkt[sizeof(struct call_pkt_ringing)];
call_send(ctx, s->group_id, from, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); struct call_pkt_ringing* r = (struct call_pkt_ringing*)pkt;
r->subcmd = CALL_SUBCMD_RINGING;
r->call_id = call_id;
call_send(ctx, s->group_id, from, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx ignored (state=%s)",
CALL_ID, (unsigned long long)call_id, call_state_name(s->state));
}
return; return;
} }
@ -555,6 +578,9 @@ static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t gro
call_event_incoming(ctx, group, from, call_id); call_event_incoming(ctx, group, from, call_id);
call_post_bgp_path(ctx, s); call_post_bgp_path(ctx, s);
/* watchdog: вызывающий шлёт кодограммы INVITE каждые 300мс; 2с тишины → вызов сорван */
s->ring_timer = uasync_set_timeout(ctx->inst->ua, CALL_RING_DEAD_TB, s, call_ring_dead_cb, "call_ring_dead");
uint8_t pkt[sizeof(struct call_pkt_ringing)]; uint8_t pkt[sizeof(struct call_pkt_ringing)];
struct call_pkt_ringing* r = (struct call_pkt_ringing*)pkt; struct call_pkt_ringing* r = (struct call_pkt_ringing*)pkt;
r->subcmd = CALL_SUBCMD_RINGING; r->subcmd = CALL_SUBCMD_RINGING;
@ -569,7 +595,7 @@ static void call_handle_ringing(struct call_ctx* ctx, uint64_t from, const uint8
if (!s || s->role != CALL_ROLE_CALLER || s->state == CALL_ST_ENDED) return; if (!s || s->role != CALL_ROLE_CALLER || s->state == CALL_ST_ENDED) return;
if (s->state == CALL_ST_OUTGOING) { if (s->state == CALL_ST_OUTGOING) {
s->state = CALL_ST_RINGING; s->state = CALL_ST_RINGING;
if (s->invite_timer) { uasync_cancel_timeout(ctx->inst->ua, s->invite_timer); s->invite_timer = NULL; } /* invite_timer не трогаем — кодограмма INVITE продолжается и в RINGING */
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: RINGING id=%016llx", CALL_ID, (unsigned long long)call_id); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: RINGING id=%016llx", CALL_ID, (unsigned long long)call_id);
call_event_id(ctx, CHAT_EVT_CALL_RINGING, call_id, 0); call_event_id(ctx, CHAT_EVT_CALL_RINGING, call_id, 0);
} }
@ -585,6 +611,7 @@ static void call_handle_accept(struct call_ctx* ctx, uint64_t from, const uint8_
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: ACCEPT id=%016llx -> conn_mgr_open", CALL_ID, (unsigned long long)call_id); DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: ACCEPT id=%016llx -> conn_mgr_open", CALL_ID, (unsigned long long)call_id);
s->state = CALL_ST_CONNECTING; s->state = CALL_ST_CONNECTING;
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; }
if (s->invite_timer) { uasync_cancel_timeout(ctx->inst->ua, s->invite_timer); s->invite_timer = NULL; }
int rc = conn_mgr_open(ctx->inst, s->group_id, s->peer_node_id, call_conn_cb, s, &s->cm_handle); int rc = conn_mgr_open(ctx->inst, s->group_id, s->peer_node_id, call_conn_cb, s, &s->cm_handle);
if (rc != 0) { if (rc != 0) {
@ -900,6 +927,7 @@ void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id) {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_accept id=%016llx -> ACCEPT + conn_mgr_open", DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_accept id=%016llx -> ACCEPT + conn_mgr_open",
CALL_ID, (unsigned long long)call_id); CALL_ID, (unsigned long long)call_id);
s->state = CALL_ST_CONNECTING; s->state = CALL_ST_CONNECTING;
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } /* watchdog кодограмм */
uint8_t pkt[sizeof(struct call_pkt_accept)]; uint8_t pkt[sizeof(struct call_pkt_accept)];
struct call_pkt_accept* a = (struct call_pkt_accept*)pkt; struct call_pkt_accept* a = (struct call_pkt_accept*)pkt;

7
src/call/call_proto.h

@ -84,9 +84,10 @@ struct call_pkt_media {
#define CALL_OPUS_FRAME_SAMPLES (CALL_OPUS_SAMPLE_RATE * CALL_OPUS_FRAME_MS / 1000) /* 960 */ #define CALL_OPUS_FRAME_SAMPLES (CALL_OPUS_SAMPLE_RATE * CALL_OPUS_FRAME_MS / 1000) /* 960 */
/* ── таймауты и ёмкости (timebase 0.1ms) ── */ /* ── таймауты и ёмкости (timebase 0.1ms) ── */
#define CALL_RING_TIMEOUT_TB 450000 /* 45с — нет ответа → CANCEL */ #define CALL_RING_TIMEOUT_TB 450000 /* 45с — вызывающий: нет ответа → CANCEL */
#define CALL_INVITE_RETRY_TB 20000 /* 2с — период ретрансмита INVITE (до RINGING) */ #define CALL_INVITE_RETRY_TB 3000 /* 300мс — кодограмма INVITE (OUTGOING + RINGING) */
#define CALL_NO_ROUTE_MAX_STRIKES 3 /* 3 × 2с ≈ 6с без маршрута → fail-fast CONNECT_FAIL */ #define CALL_RING_DEAD_TB 20000 /* 2с — вызываемый: нет кодограммы → вызов сорван */
#define CALL_NO_ROUTE_MAX_STRIKES 20 /* 20 × 300мс ≈ 6с без маршрута → fail-fast CONNECT_FAIL */
#define CALL_NO_TRAFFIC_TB 200000 /* 20с — нет входящих медиа → HANGUP */ #define CALL_NO_TRAFFIC_TB 200000 /* 20с — нет входящих медиа → HANGUP */
#define CALL_TEARDOWN_DELAY_TB 5000 /* 500мс — задержка разрыва conn_mgr после завершения */ #define CALL_TEARDOWN_DELAY_TB 5000 /* 500мс — задержка разрыва conn_mgr после завершения */
#define CALL_MAX_TX_FRAMES 50 /* ~1с буфер отправки (50 кадров × 20мс) */ #define CALL_MAX_TX_FRAMES 50 /* ~1с буфер отправки (50 кадров × 20мс) */

123
tools/chatgui-android/app/src/main/java/com/utun/chat/data/RingtonePlayer.kt

@ -16,12 +16,18 @@ import com.utun.chat.ChatApplication
import com.utun.chat.R import com.utun.chat.R
/** /**
* Мелодия входящего звонка через профиль звонка (STREAM_RING): * Мелодия входящего звонка через профиль звонка (STREAM_RING) + ручная вибрация.
* - громкость — настройка «громкость вызова»;
* - вибрация — по системной настройке «вибрировать при вызове» (VIBRATE_WHEN_RINGING).
* *
* API 28+ : Ringtone (RingtoneManager) — громкость и вибрация автоматически. * Поведение по системному режиму звонка (AudioManager.getRingerMode):
* API 26–27: MediaPlayer(STREAM_RING, loop) + ручная вибрация по системной настройке. * - NORMAL : звук + вибрация, если включена настройка «вибрировать при вызове»
* (Settings.System.VIBRATE_WHEN_RINGING);
* - VIBRATE: без звука, только вибрация;
* - SILENT : без звука и вибрации.
*
* Вибрация всегда запускается вручную через Vibrator (класс Ringtone сам не вибрирует).
*
* API 28+ : звук через Ringtone (RingtoneManager), громкость — «громкость вызова».
* API 26–27: звук через MediaPlayer(STREAM_RING, loop) + audio focus.
*/ */
class RingtonePlayer { class RingtonePlayer {
@ -33,50 +39,69 @@ class RingtonePlayer {
fun play() { fun play() {
stop() stop()
val ctx = ChatApplication.instance val ctx = ChatApplication.instance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val am = try {
val uri = Uri.parse("android.resource://${ctx.packageName}/${R.raw.ringtone}") ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val r = try { } catch (e: Exception) {
RingtoneManager.getRingtone(ctx, uri) LogManager.addLog("WARN", "Ringtone", "audio service unavailable: ${e.message}")
} catch (e: Exception) { null
LogManager.addLog("ERROR", "Ringtone", "getRingtone failed: ${e.message}") } ?: return
null
} ?: return val ringerMode = am.ringerMode
try { val playSound = ringerMode == AudioManager.RINGER_MODE_NORMAL
r.isLooping = true val playVibrate = ringerMode == AudioManager.RINGER_MODE_VIBRATE
r.play() || (playSound && systemVibrateWhenRinging())
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "play failed: ${e.message}") LogManager.addLog("INFO", "Ringtone",
return "ringerMode=%d sound=%b vibrate=%b".format(ringerMode, playSound, playVibrate))
}
ringtone = r if (playSound) {
LogManager.addLog("INFO", "Ringtone", "started (Ringtone, STREAM_RING + system vibrate)") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
} else { val uri = Uri.parse("android.resource://${ctx.packageName}/${R.raw.ringtone}")
val mp = try { val r = try {
MediaPlayer.create(ctx, R.raw.ringtone) RingtoneManager.getRingtone(ctx, uri)
} catch (e: Exception) { } catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer.create failed: ${e.message}") LogManager.addLog("ERROR", "Ringtone", "getRingtone failed: ${e.message}")
null null
} ?: return } ?: return
try { try {
mp.setAudioAttributes( r.isLooping = true
AudioAttributes.Builder() r.play()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) } catch (e: Exception) {
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) LogManager.addLog("ERROR", "Ringtone", "play failed: ${e.message}")
.setLegacyStreamType(AudioManager.STREAM_RING) return
.build() }
) ringtone = r
mp.isLooping = true LogManager.addLog("INFO", "Ringtone", "sound started (Ringtone, STREAM_RING)")
requestAudioFocus() } else {
mp.start() val mp = try {
} catch (e: Exception) { MediaPlayer.create(ctx, R.raw.ringtone)
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer start failed: ${e.message}") } catch (e: Exception) {
try { mp.release() } catch (_: Exception) {} LogManager.addLog("ERROR", "Ringtone", "MediaPlayer.create failed: ${e.message}")
abandonAudioFocus() null
return } ?: return
try {
mp.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_RING)
.build()
)
mp.isLooping = true
requestAudioFocus()
mp.start()
} catch (e: Exception) {
LogManager.addLog("ERROR", "Ringtone", "MediaPlayer start failed: ${e.message}")
try { mp.release() } catch (_: Exception) {}
abandonAudioFocus()
return
}
mediaPlayer = mp
LogManager.addLog("INFO", "Ringtone", "sound started (MediaPlayer fallback, STREAM_RING)")
} }
mediaPlayer = mp }
if (playVibrate) {
startVibrate() startVibrate()
LogManager.addLog("INFO", "Ringtone", "started (MediaPlayer fallback, STREAM_RING)")
} }
} }
@ -95,7 +120,7 @@ class RingtonePlayer {
LogManager.addLog("INFO", "Ringtone", "stopped") LogManager.addLog("INFO", "Ringtone", "stopped")
} }
/* ── вибрация (fallback API 26–27) по системной настройке ── */ /* ── вибрация (решение принято в play(): VIBRATE-режим или VIBRATE_WHEN_RINGING) ── */
private fun systemVibrateWhenRinging(): Boolean = try { private fun systemVibrateWhenRinging(): Boolean = try {
Settings.System.getInt( Settings.System.getInt(
@ -105,7 +130,6 @@ class RingtonePlayer {
} }
private fun startVibrate() { private fun startVibrate() {
if (!systemVibrateWhenRinging()) return
val v = try { val v = try {
ChatApplication.instance.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator ChatApplication.instance.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
} catch (e: Exception) { } catch (e: Exception) {
@ -116,6 +140,7 @@ class RingtonePlayer {
try { try {
v.vibrate(VibrationEffect.createWaveform(longArrayOf(1000, 1000), 0)) v.vibrate(VibrationEffect.createWaveform(longArrayOf(1000, 1000), 0))
vibrator = v vibrator = v
LogManager.addLog("INFO", "Ringtone", "vibration started")
} catch (e: Exception) { } catch (e: Exception) {
LogManager.addLog("WARN", "Ringtone", "vibrate failed: ${e.message}") LogManager.addLog("WARN", "Ringtone", "vibrate failed: ${e.message}")
} }

Loading…
Cancel
Save