|
| 1 | +package com.evsuite.chargepilot |
| 2 | + |
| 3 | +import android.Manifest |
| 4 | +import android.annotation.SuppressLint |
| 5 | +import android.app.NotificationChannel |
| 6 | +import android.app.NotificationManager |
| 7 | +import android.app.PendingIntent |
| 8 | +import android.content.Context |
| 9 | +import android.content.Intent |
| 10 | +import android.content.pm.PackageManager |
| 11 | +import android.os.Build |
| 12 | +import androidx.core.app.NotificationCompat |
| 13 | +import androidx.core.app.NotificationManagerCompat |
| 14 | +import androidx.core.content.ContextCompat |
| 15 | +import com.evsuite.hardware.AppLogger |
| 16 | +import com.evsuite.hardware.telemetry.EnergySnapshot |
| 17 | + |
| 18 | +/** |
| 19 | + * Tells the driver once that every validation question has an answer, so the bundle is worth |
| 20 | + * exporting now rather than after another drive spent guessing. |
| 21 | + * |
| 22 | + * Parked only, with the same gate as the battery advice: the questions mostly answer |
| 23 | + * themselves on the road, and a notice that pops up at speed is the overlay the suite forbids. |
| 24 | + * Once per process — the rings are in memory, so a restart starts the count again anyway. |
| 25 | + */ |
| 26 | +internal object ValidationReadyNotice { |
| 27 | + |
| 28 | + @Volatile |
| 29 | + private var posted = false |
| 30 | + |
| 31 | + /** On the sampler thread, every sample; one flag and one set comparison until it fires. */ |
| 32 | + fun onSample(context: Context, text: () -> Context, value: EnergySnapshot) { |
| 33 | + if (posted || !ValidationProbe.allAnswered()) return |
| 34 | + val gate = ParkedDeletionPolicy.gate(value.speedKmh, value.timestampMs, System.currentTimeMillis()) |
| 35 | + if (gate != ParkedDeletionGate.PARKED) return |
| 36 | + posted = true |
| 37 | + AppLogger.i(TAG, "validation: every question answered, export suggested") |
| 38 | + runCatching { post(context.applicationContext, text()) } |
| 39 | + .onFailure { AppLogger.w(TAG, "validation notice failed: ${it.message}") } |
| 40 | + } |
| 41 | + |
| 42 | + // API 28 head unit: no permission gates a notification. On API 33+ without |
| 43 | + // POST_NOTIFICATIONS (not declared) nothing is posted; the log line above still says it. |
| 44 | + @SuppressLint("MissingPermission", "NotificationPermission") |
| 45 | + private fun post(context: Context, res: Context) { |
| 46 | + val manager = NotificationManagerCompat.from(context) |
| 47 | + if (!manager.areNotificationsEnabled()) return |
| 48 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && |
| 49 | + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != |
| 50 | + PackageManager.PERMISSION_GRANTED |
| 51 | + ) return |
| 52 | + context.getSystemService(NotificationManager::class.java)?.createNotificationChannel( |
| 53 | + NotificationChannel( |
| 54 | + CHANNEL_ID, |
| 55 | + res.getString(R.string.validation_ready_channel), |
| 56 | + NotificationManager.IMPORTANCE_DEFAULT, |
| 57 | + ) |
| 58 | + ) |
| 59 | + val tap = PendingIntent.getActivity( |
| 60 | + context, 0, |
| 61 | + Intent(context, DiagnosticsActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), |
| 62 | + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, |
| 63 | + ) |
| 64 | + val body = res.getString(R.string.validation_ready_text) |
| 65 | + manager.notify( |
| 66 | + NOTIFICATION_ID, |
| 67 | + NotificationCompat.Builder(context, CHANNEL_ID) |
| 68 | + .setSmallIcon(R.drawable.ic_launcher_monochrome) |
| 69 | + .setContentTitle(res.getString(R.string.validation_ready_title)) |
| 70 | + .setContentText(body) |
| 71 | + .setStyle(NotificationCompat.BigTextStyle().bigText(body)) |
| 72 | + .setContentIntent(tap) |
| 73 | + .setAutoCancel(true) |
| 74 | + .build(), |
| 75 | + ) |
| 76 | + } |
| 77 | + |
| 78 | + private const val TAG = "EVChargePilot" |
| 79 | + private const val CHANNEL_ID = "validation_ready" |
| 80 | + private const val NOTIFICATION_ID = 200 |
| 81 | +} |
0 commit comments