package com.glosira.app import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.BitmapFactory import android.os.Build import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.PeriodicWorkRequestBuilder import androidx.work.Worker import androidx.work.WorkManager import androidx.work.WorkerParameters import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.TimeUnit // Чистовой воркер напоминаний: раз в час дёргает GET /api/private/reminder с токеном. // РЕШЕНИЕ, слать ли пуш, принимает БЭК (по streak_at/троттлингу): 200 -> показываем, // 204/пусто -> ничего. Токен и база API кладутся из JS в SharedPreferences (мост). class ReminderWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override fun doWork(): Result { val prefs = applicationContext.getSharedPreferences("glosira", Context.MODE_PRIVATE) if (!prefs.getBoolean("notifications_enabled", true)) return Result.success() // выключено в настройках val token = prefs.getString("token", "") ?: "" val apiBase = prefs.getString("api_base", "") ?: "" if (token.isEmpty() || apiBase.isEmpty()) return Result.success() // не залогинен → тихо var conn: HttpURLConnection? = null try { conn = URL(apiBase.trimEnd('/') + "/private/reminder").openConnection() as HttpURLConnection conn.setRequestProperty("Authorization", token) conn.connectTimeout = 8000 conn.readTimeout = 8000 val code = conn.responseCode if (code == 200) { val body = conn.inputStream.bufferedReader().use { it.readText() } if (body.isNotBlank()) { val j = JSONObject(body) val text = j.optString("text") if (text.isNotEmpty()) showNotification(applicationContext, j.optString("character"), j.optString("name"), text) } return Result.success() } if (code in 500..599) return Result.retry() // временная серверная ошибка → повтор с backoff return Result.success() // 204/4xx — не повторяем } catch (e: Exception) { return Result.retry() // сеть/таймаут — повтор с backoff } finally { conn?.disconnect() // соединение всегда закрываем } } companion object { private const val CHANNEL = "reminders" // character из ответа (lowercase животное) → лицо и имя персонажа. private fun faceRes(animal: String): Int = when (animal) { "badger" -> R.drawable.face_badger "cat" -> R.drawable.face_cat "fox" -> R.drawable.face_fox "panda" -> R.drawable.face_panda "raven" -> R.drawable.face_raven "rhino" -> R.drawable.face_rhino "sheep" -> R.drawable.face_sheep "sloth" -> R.drawable.face_sloth "wolf" -> R.drawable.face_wolf else -> 0 } // Имя персонажа НЕ хардкодим по-русски — приходит с бэка (мультиязычно: "Полли"/"Polly"). fun showNotification(ctx: Context, animal: String, name: String, text: String) { val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { nm.createNotificationChannel( NotificationChannel(CHANNEL, "Напоминания", NotificationManager.IMPORTANCE_DEFAULT) ) } val intent = Intent(ctx, MainActivity::class.java) val piFlags = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0 val pi = PendingIntent.getActivity(ctx, 0, intent, piFlags) @Suppress("DEPRECATION") val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(ctx, CHANNEL) else Notification.Builder(ctx) builder.setSmallIcon(R.drawable.ic_notification) .setContentTitle(name.ifEmpty { "Glosira" }) .setContentText(text) .setStyle(Notification.BigTextStyle().bigText(text)) // длинные фразы видно целиком .setAutoCancel(true) .setContentIntent(pi) val face = faceRes(animal) if (face != 0) { val bmp = BitmapFactory.decodeResource(ctx.resources, face) if (bmp != null) builder.setLargeIcon(bmp) } // Фиксированный id: новое напоминание заменяет прошлое непрочитанное (не копим старые нудежи). nm.notify(1001, builder.build()) } } } object ReminderScheduler { fun schedule(ctx: Context) { val wm = WorkManager.getInstance(ctx) // Запускать только при наличии сети — иначе WorkManager отложит и корректно отработает backoff. val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() // Раз в час. KEEP — не сбрасывать таймер при каждом запуске приложения. // Backoff: при retry (сеть/5xx) повтор с экспонентой от 30с. val periodic = PeriodicWorkRequestBuilder(1, TimeUnit.HOURS) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) .build() wm.enqueueUniquePeriodicWork("glosira_reminder", ExistingPeriodicWorkPolicy.KEEP, periodic) } }