From 60627f11d64e79f70fc2b074938f9f2cfd5e12f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Mon, 11 May 2026 22:43:22 +0300 Subject: [PATCH] feat(app): deep links, notification launch, and night splash Add wallenc:// URI patterns, manifest VIEW intent-filters, and NavDeepLink with handleDeepLink from MainActivity. Move FGS notification PendingIntent constants to WallencExternalLaunch. Theme splash/window background via splash_screen_background in values and values-night. Co-authored-by: Cursor --- app/src/main/AndroidManifest.xml | 8 ++++ .../nullptroma/wallenc/app/MainActivity.kt | 32 +++++++------ .../app/navigation/WallencExternalLaunch.kt | 43 ++++++++++++++++++ .../tasks/TaskPipelineForegroundService.kt | 45 +++++++++---------- app/src/main/res/values-night/colors.xml | 5 +++ app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/themes.xml | 12 +++-- .../github/nullptroma/wallenc/ui/WallencUi.kt | 43 +++++++++++++----- .../wallenc/ui/navigation/WallencDeepLinks.kt | 29 ++++++++++++ 9 files changed, 170 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/github/nullptroma/wallenc/app/navigation/WallencExternalLaunch.kt create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 ui/src/main/java/com/github/nullptroma/wallenc/ui/navigation/WallencDeepLinks.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 59a7612..8a81c59 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,14 @@ + + + + + + + + { repeat = false if (sawVisible) { - nm.cancel(FOREGROUND_NOTIFICATION_ID) + nm.cancel(WallencExternalLaunch.ForegroundTaskPipelineNotification.NOTIFICATION_ID) indeterminateDotsPhaseByTaskId.clear() stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() @@ -97,7 +102,7 @@ class TaskPipelineForegroundService : Service() { if (ui.tasks.isNotEmpty()) { sawVisible = true nm.notify( - FOREGROUND_NOTIFICATION_ID, + WallencExternalLaunch.ForegroundTaskPipelineNotification.NOTIFICATION_ID, buildAccumulatedNotification(ui.tasks), ) } @@ -129,7 +134,7 @@ class TaskPipelineForegroundService : Service() { private fun ensureChannel() { val nm = getSystemService(NotificationManager::class.java) val channel = NotificationChannel( - CHANNEL_ID, + WallencExternalLaunch.ForegroundTaskPipelineNotification.CHANNEL_ID, getString(R.string.task_notification_channel_name), NotificationManager.IMPORTANCE_LOW, ) @@ -137,7 +142,7 @@ class TaskPipelineForegroundService : Service() { } private fun buildPlaceholderNotification(): Notification = - NotificationCompat.Builder(this, CHANNEL_ID) + NotificationCompat.Builder(this, WallencExternalLaunch.ForegroundTaskPipelineNotification.CHANNEL_ID) .setContentTitle(getString(R.string.task_notification_title)) .setContentText(getString(R.string.task_notification_preparing)) .setSmallIcon(android.R.drawable.stat_sys_download) @@ -171,7 +176,10 @@ class TaskPipelineForegroundService : Service() { ) } - return NotificationCompat.Builder(this, CHANNEL_ID) + return NotificationCompat.Builder( + this, + WallencExternalLaunch.ForegroundTaskPipelineNotification.CHANNEL_ID, + ) .setContentTitle(getString(R.string.task_notification_title)) .setContentText(collapsedSubtext) .setSmallIcon(android.R.drawable.stat_sys_download) @@ -191,19 +199,18 @@ class TaskPipelineForegroundService : Service() { private fun openTaskPipelinePendingIntent(): PendingIntent = PendingIntent.getActivity( this, - REQUEST_CODE_OPEN_TASK_PIPELINE, - Intent(this, MainActivity::class.java).apply { - putExtra(MainActivity.EXTRA_OPEN_TASK_PIPELINE, true) - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP - }, + WallencExternalLaunch.PendingIntentRequestCodes.NOTIFICATION_TASK_PIPELINE_OPEN_TASKS, + WallencExternalLaunch.mainActivityViewIntentForTasks(this), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) private fun cancelAllTasksPendingIntent(): PendingIntent = PendingIntent.getService( this, - 0, - Intent(this, TaskPipelineForegroundService::class.java).setAction(ACTION_CANCEL_ALL_TASKS), + WallencExternalLaunch.PendingIntentRequestCodes.NOTIFICATION_TASK_PIPELINE_CANCEL_SERVICE, + Intent(this, TaskPipelineForegroundService::class.java).setAction( + WallencExternalLaunch.FOREGROUND_SERVICE_ACTION_CANCEL_ALL_TASKS, + ), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) @@ -334,14 +341,6 @@ class TaskPipelineForegroundService : Service() { } companion object { - private const val ACTION_CANCEL_ALL_TASKS = - "com.github.nullptroma.wallenc.action.CANCEL_ALL_TASKS" - - private const val REQUEST_CODE_OPEN_TASK_PIPELINE = 2 - - private const val CHANNEL_ID = "wallenc_task_pipeline" - private const val FOREGROUND_NOTIFICATION_ID = 1001 - private const val NOTIFICATION_QUEUE_STEP_MS = 500L /** Must match [R.layout.notification_wallenc_tasks_big] row count. */ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..dffd973 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + + #FF1C1B1F + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 545704f..32c62cc 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,2 +1,4 @@ - + + #FFFFFFFF + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e6bc520..25b0609 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,11 @@ - + - + diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/WallencUi.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/WallencUi.kt index 97a0d49..f974f8a 100644 --- a/ui/src/main/java/com/github/nullptroma/wallenc/ui/WallencUi.kt +++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/WallencUi.kt @@ -1,5 +1,6 @@ package com.github.nullptroma.wallenc.ui +import android.content.Intent import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -26,7 +27,10 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.navDeepLink import com.github.nullptroma.wallenc.ui.navigation.NavBarItemData +import com.github.nullptroma.wallenc.ui.navigation.WallencDeepLinks +import com.github.nullptroma.wallenc.ui.navigation.matchesWallencDeepLink import com.github.nullptroma.wallenc.ui.navigation.rememberNavigationState import com.github.nullptroma.wallenc.ui.screens.main.MainRoute import com.github.nullptroma.wallenc.ui.screens.main.MainScreen @@ -40,10 +44,16 @@ import com.github.nullptroma.wallenc.ui.theme.WallencTheme @Composable -fun WallencUi(taskPipelineOpenRequestCount: Int = 0) { +fun WallencUi( + deepLinkPulse: Int = 0, + deepLinkIntent: Intent = Intent(), +) { WallencTheme { Surface { - WallencNavRoot(taskPipelineOpenRequestCount = taskPipelineOpenRequestCount) + WallencNavRoot( + deepLinkPulse = deepLinkPulse, + deepLinkIntent = deepLinkIntent, + ) } } } @@ -52,7 +62,8 @@ fun WallencUi(taskPipelineOpenRequestCount: Int = 0) { @Composable fun WallencNavRoot( viewModel: WallencViewModel = hiltViewModel(), - taskPipelineOpenRequestCount: Int = 0, + deepLinkPulse: Int = 0, + deepLinkIntent: Intent = Intent(), ) { val navState = rememberNavigationState() val mainNavState = rememberNavigationState() @@ -62,10 +73,10 @@ fun WallencNavRoot( val topLevelRoutes = viewModel.routes - LaunchedEffect(taskPipelineOpenRequestCount) { - if (taskPipelineOpenRequestCount <= 0) return@LaunchedEffect - val route = topLevelRoutes[TaskPipelineRoute::class.qualifiedName!!] ?: return@LaunchedEffect - navState.changeTop(route) + LaunchedEffect(deepLinkPulse) { + if (deepLinkPulse <= 0) return@LaunchedEffect + if (!deepLinkIntent.matchesWallencDeepLink()) return@LaunchedEffect + navState.navHostController.handleDeepLink(deepLinkIntent) } val topLevelNavBarItems = remember { @@ -121,7 +132,11 @@ fun WallencNavRoot( navState.navHostController, startDestination = topLevelRoutes[MainRoute::class.qualifiedName]!! ) { - composable(enterTransition = { + composable( + deepLinks = listOf( + navDeepLink { uriPattern = WallencDeepLinks.MAIN_URI_PATTERN }, + ), + enterTransition = { fadeIn(tween(200)) }, exitTransition = { fadeOut(tween(200)) @@ -132,14 +147,22 @@ fun WallencNavRoot( viewModel = mainViewModel ) } - composable(enterTransition = { + composable( + deepLinks = listOf( + navDeepLink { uriPattern = WallencDeepLinks.SETTINGS_URI_PATTERN }, + ), + enterTransition = { fadeIn(tween(200)) }, exitTransition = { fadeOut(tween(200)) }) { SettingsScreen(Modifier.padding(innerPaddings), settingsViewModel) } - composable(enterTransition = { + composable( + deepLinks = listOf( + navDeepLink { uriPattern = WallencDeepLinks.TASKS_URI_PATTERN }, + ), + enterTransition = { fadeIn(tween(200)) }, exitTransition = { fadeOut(tween(200)) diff --git a/ui/src/main/java/com/github/nullptroma/wallenc/ui/navigation/WallencDeepLinks.kt b/ui/src/main/java/com/github/nullptroma/wallenc/ui/navigation/WallencDeepLinks.kt new file mode 100644 index 0000000..7c5c1e1 --- /dev/null +++ b/ui/src/main/java/com/github/nullptroma/wallenc/ui/navigation/WallencDeepLinks.kt @@ -0,0 +1,29 @@ +package com.github.nullptroma.wallenc.ui.navigation + +import android.content.Intent + +/** + * URI для входа извне приложения (уведомления, виджеты, шорткаты, другие Activity). + * + * Должны совпадать с `` в манифесте + * и с [androidx.navigation.navDeepLink] на соответствующих `composable` в [androidx.navigation.compose.NavHost]. + * + * Для публичных HTTPS-ссылок позже добавляют отдельные хосты и `android:autoVerify`. + */ +object WallencDeepLinks { + const val SCHEME = "wallenc" + + object Host { + const val MAIN = "main" + const val TASKS = "tasks" + const val SETTINGS = "settings" + } + + const val MAIN_URI_PATTERN = "$SCHEME://${Host.MAIN}" + const val TASKS_URI_PATTERN = "$SCHEME://${Host.TASKS}" + const val SETTINGS_URI_PATTERN = "$SCHEME://${Host.SETTINGS}" +} + +/** `ACTION_VIEW` с URI нашей схемы — обрабатывается [androidx.navigation.NavController.handleDeepLink]. */ +fun Intent.matchesWallencDeepLink(): Boolean = + action == Intent.ACTION_VIEW && data?.scheme == WallencDeepLinks.SCHEME