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 <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 22:43:22 +03:00
parent 88a13080e5
commit 60627f11d6
9 changed files with 170 additions and 51 deletions

View File

@@ -28,6 +28,14 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="wallenc" android:host="main" />
<data android:scheme="wallenc" android:host="tasks" />
<data android:scheme="wallenc" android:host="settings" />
</intent-filter>
</activity>
<service

View File

@@ -1,18 +1,22 @@
package com.github.nullptroma.wallenc.app
import android.Manifest
import android.content.pm.PackageManager
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.github.nullptroma.wallenc.app.auth.YandexSignInService
import com.github.nullptroma.wallenc.ui.WallencUi
import com.github.nullptroma.wallenc.ui.navigation.matchesWallencDeepLink
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
@@ -24,7 +28,8 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var yandexSignInService: YandexSignInService
private val openTaskPipelineFromNotification = mutableIntStateOf(0)
private val deepLinkPulse = mutableIntStateOf(0)
private var deepLinkIntentForNav: Intent by mutableStateOf(Intent())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -34,11 +39,16 @@ class MainActivity : ComponentActivity() {
Timber.plant(Timber.DebugTree())
consumeOpenTaskPipelineIntent(intent)
deepLinkIntentForNav = intent
if (intent.matchesWallencDeepLink()) {
deepLinkPulse.intValue++
}
setContent {
val pulse by deepLinkPulse
WallencUi(
taskPipelineOpenRequestCount = openTaskPipelineFromNotification.intValue,
deepLinkPulse = pulse,
deepLinkIntent = deepLinkIntentForNav,
)
}
}
@@ -46,13 +56,10 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
consumeOpenTaskPipelineIntent(intent)
}
private fun consumeOpenTaskPipelineIntent(intent: Intent?) {
if (intent?.getBooleanExtra(EXTRA_OPEN_TASK_PIPELINE, false) != true) return
intent.removeExtra(EXTRA_OPEN_TASK_PIPELINE)
openTaskPipelineFromNotification.intValue += 1
deepLinkIntentForNav = intent
if (intent.matchesWallencDeepLink()) {
deepLinkPulse.intValue++
}
}
override fun onDestroy() {
@@ -75,9 +82,6 @@ class MainActivity : ComponentActivity() {
}
companion object {
const val EXTRA_OPEN_TASK_PIPELINE =
"com.github.nullptroma.wallenc.app.EXTRA_OPEN_TASK_PIPELINE"
private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 100
}
}

View File

@@ -0,0 +1,43 @@
package com.github.nullptroma.wallenc.app.navigation
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.nullptroma.wallenc.app.MainActivity
import com.github.nullptroma.wallenc.ui.navigation.WallencDeepLinks
/**
* Запуск [MainActivity] и взаимодействие с foreground-сервисом из **вне** Compose: [android.app.PendingIntent],
* стабильные action, id канала/нотификации.
*
* URI см. [WallencDeepLinks] в `:ui` — тот же контракт, что у [androidx.navigation.navDeepLink] и манифеста.
*/
object WallencExternalLaunch {
/** Коды [android.app.PendingIntent]: у каждого сценария свой requestCode. */
object PendingIntentRequestCodes {
const val NOTIFICATION_TASK_PIPELINE_CANCEL_SERVICE = 0
const val NOTIFICATION_TASK_PIPELINE_OPEN_TASKS = 2
}
/**
* [Intent.getAction] для [com.github.nullptroma.wallenc.app.tasks.TaskPipelineForegroundService]
* из кнопки уведомления ([PendingIntent.getService]).
*/
const val FOREGROUND_SERVICE_ACTION_CANCEL_ALL_TASKS =
"com.github.nullptroma.wallenc.action.CANCEL_ALL_TASKS"
object ForegroundTaskPipelineNotification {
const val CHANNEL_ID = "wallenc_task_pipeline"
const val NOTIFICATION_ID = 1001
}
fun mainActivityViewIntent(context: Context, uri: Uri): Intent =
Intent(Intent.ACTION_VIEW, uri).apply {
setClass(context, MainActivity::class.java)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
fun mainActivityViewIntentForTasks(context: Context): Intent =
mainActivityViewIntent(context, Uri.parse(WallencDeepLinks.TASKS_URI_PATTERN))
}

View File

@@ -11,8 +11,8 @@ import android.os.IBinder
import android.view.View
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import com.github.nullptroma.wallenc.app.MainActivity
import com.github.nullptroma.wallenc.app.R
import com.github.nullptroma.wallenc.app.navigation.WallencExternalLaunch
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundItem
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
@@ -58,7 +58,9 @@ class TaskPipelineForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_CANCEL_ALL_TASKS && ::orchestrator.isInitialized) {
if (intent?.action == WallencExternalLaunch.FOREGROUND_SERVICE_ACTION_CANCEL_ALL_TASKS &&
::orchestrator.isInitialized
) {
orchestrator.cancelAll()
}
return START_STICKY
@@ -68,7 +70,10 @@ class TaskPipelineForegroundService : Service() {
override fun onCreate() {
super.onCreate()
ensureChannel()
startForeground(FOREGROUND_NOTIFICATION_ID, buildPlaceholderNotification())
startForeground(
WallencExternalLaunch.ForegroundTaskPipelineNotification.NOTIFICATION_ID,
buildPlaceholderNotification(),
)
val nm = getSystemService(NotificationManager::class.java)
@@ -85,7 +90,7 @@ class TaskPipelineForegroundService : Service() {
TaskForegroundUiState.Hidden -> {
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. */

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Тёмная тема: фон splash и окна до отрисовки Compose -->
<color name="splash_screen_background">#FF1C1B1F</color>
</resources>

View File

@@ -1,2 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources />
<resources>
<color name="splash_screen_background">#FFFFFFFF</color>
</resources>

View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Wallenc" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
<style name="Theme.Wallenc" parent="android:Theme.Material.Light.NoActionBar">
<!-- До первого кадра Compose и системный splash (12+) -->
<item name="android:windowBackground">@color/splash_screen_background</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="31">
@color/splash_screen_background
</item>
</style>
</resources>