package com.shukria.kiosklauncher.util import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.FileProvider import com.shukria.kiosklauncher.R import java.io.File /** * Phase 3 — manual app upgrades (forceUpgrade = false). * * The APK is already downloaded; we only surface a notification the user taps to * run the system installer. Silent upgrades (forceUpgrade = true) never come here — * they go straight through YSDKManager.installApp(). */ object AppUpdateNotifier { private const val TAG = "AppUpdateNotifier" private const val CHANNEL_ID = "app_update_channel" private const val MIME_APK = "application/vnd.android.package-archive" /** @return true if the notification was posted, false if it could not be built/shown. */ fun showManualInstallPrompt( context: Context, apkFile: File, packageName: String, version: String ): Boolean = try { createNotificationChannel(context) val pending = PendingIntent.getActivity( context, notificationId(packageName), buildInstallIntent(context, apkFile), pendingIntentFlags() ) val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle("Update available") .setContentText("Tap to install $packageName v$version") .setSmallIcon(R.drawable.ic_launcher_foreground) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setContentIntent(pending) .build() notificationManager(context).notify(notificationId(packageName), notification) Log.i(TAG, "Manual install prompt shown for $packageName v$version") true } catch (e: Exception) { Log.e(TAG, "Failed to show manual install prompt for $packageName: ${e.message}") false } /** Called once the install is confirmed, so a stale prompt is not left behind. */ fun cancel(context: Context, packageName: String) { try { notificationManager(context).cancel(notificationId(packageName)) } catch (e: Exception) { Log.w(TAG, "Could not cancel prompt for $packageName: ${e.message}") } } private fun buildInstallIntent(context: Context, apkFile: File): Intent = Intent(Intent.ACTION_VIEW).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // file:// URIs are rejected from API 24 — hand out a content:// URI instead val uri = FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", apkFile ) setDataAndType(uri, MIME_APK) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } else { setDataAndType(Uri.fromFile(apkFile), MIME_APK) } } private fun pendingIntentFlags(): Int = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE private fun createNotificationChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "App Updates", NotificationManager.IMPORTANCE_HIGH ).apply { description = "Pending application upgrades waiting to be installed" } notificationManager(context).createNotificationChannel(channel) } } private fun notificationManager(context: Context): NotificationManager = context.getSystemService(NotificationManager::class.java) /** Stable per-package id so a re-pushed command replaces its own prompt. */ private fun notificationId(packageName: String): Int = packageName.hashCode() }