6 Commits

Author SHA1 Message Date
ua1zbe
bdf3a529a5 Bump 1.0.3: enable RuStore Push (project_id), release candidate 2026-09-23 09:27:01 +03:00
ua1zbe
28bc857788 Add RuStore broadcast push script (secrets gitignored) 2026-09-23 09:22:09 +03:00
ua1zbe
d52382ca5b Add RuStore Push integration (project_id placeholder) 2026-09-23 09:17:43 +03:00
ua1zbe
db81f6a51e Bump version to 1.0.2 2026-09-23 09:09:51 +03:00
ua1zbe
27eab81303 Keep screen on while app is active 2026-09-23 09:06:34 +03:00
ua1zbe
7c8aa2fdb2 Bump version to 1.0.1 2026-09-23 08:58:38 +03:00
11 changed files with 190 additions and 5 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ captures/
*.aab *.aab
keystore.properties keystore.properties
*.jks *.jks
tools/.push.secrets

View File

@@ -18,8 +18,8 @@ android {
applicationId = "ru.ua1zbe.aprsmap" applicationId = "ru.ua1zbe.aprsmap"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 4
versionName = "1.0.0" versionName = "1.0.3"
} }
signingConfigs { signingConfigs {
@@ -56,3 +56,7 @@ kotlin {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
} }
} }
dependencies {
implementation("ru.rustore.sdk:pushclient:7.4.0")
}

View File

@@ -1,10 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@@ -12,6 +15,22 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.APRS.Main"> android:theme="@style/Theme.APRS.Main">
<service
android:name=".PushMessagingService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="ru.rustore.sdk.pushclient.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="ru.rustore.sdk.pushclient.default_notification_icon"
android:resource="@drawable/ic_launcher_foreground" />
<meta-data
android:name="ru.rustore.sdk.pushclient.default_notification_channel_id"
android:value="@string/push_channel_id" />
<activity <activity
android:name=".SplashActivity" android:name=".SplashActivity"
android:exported="true" android:exported="true"

View File

@@ -0,0 +1,60 @@
package ru.ua1zbe.aprsmap
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import ru.rustore.sdk.pushclient.RuStorePushClient
class App : Application() {
override fun onCreate() {
super.onCreate()
createPushChannel()
initRuStorePush()
}
private fun createPushChannel() {
getSystemService(NotificationManager::class.java).createNotificationChannel(
NotificationChannel(
PushMessagingService.CHANNEL_ID,
getString(R.string.push_channel_name),
NotificationManager.IMPORTANCE_HIGH
)
)
}
private fun initRuStorePush() {
if (RUSTORE_PROJECT_ID.isBlank()) {
Log.w(TAG, "RuStore project_id not set yet, init skipped")
return
}
try {
RuStorePushClient.init(application = this, projectId = RUSTORE_PROJECT_ID)
RuStorePushClient.getToken()
.addOnSuccessListener { token ->
Log.i(TAG, "push token: $token")
saveToken(token)
RuStorePushClient.subscribeToTopic(PUSH_TOPIC)
.addOnSuccessListener { Log.i(TAG, "subscribed to $PUSH_TOPIC") }
.addOnFailureListener { e -> Log.w(TAG, "subscribe failed", e) }
}
.addOnFailureListener { e -> Log.w(TAG, "getToken failed", e) }
} catch (e: Exception) {
Log.w(TAG, "init failed", e)
}
}
private fun saveToken(token: String) {
getSharedPreferences(PREFS, MODE_PRIVATE)
.edit().putString("push_token", token).apply()
}
companion object {
private const val TAG = "RuStorePush"
private const val RUSTORE_PROJECT_ID = "IiZJx8LXrFuVclMLbX08nfrShS9WWB6k"
private const val PUSH_TOPIC = "aprsmap"
const val PREFS = "aprsmap"
}
}

View File

@@ -1,9 +1,11 @@
package ru.ua1zbe.aprsmap package ru.ua1zbe.aprsmap
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
@@ -16,6 +18,7 @@ import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
@@ -39,6 +42,7 @@ class MainActivity : Activity() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val root = FrameLayout(this).apply { val root = FrameLayout(this).apply {
setBackgroundColor(Color.rgb(6, 11, 20)) setBackgroundColor(Color.rgb(6, 11, 20))
@@ -102,10 +106,24 @@ class MainActivity : Activity() {
root.addView(progress) root.addView(progress)
setContentView(root) setContentView(root)
requestNotificationPermission()
webView.loadUrl(getString(R.string.map_url)) webView.loadUrl(getString(R.string.map_url))
checkForUpdates() checkForUpdates()
} }
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQ_NOTIFICATIONS
)
}
}
private fun checkForUpdates() { private fun checkForUpdates() {
Thread { Thread {
val version = try { val version = try {
@@ -235,5 +253,6 @@ class MainActivity : Activity() {
companion object { companion object {
private const val TAG = "APRSmap" private const val TAG = "APRSmap"
private const val REQ_NOTIFICATIONS = 100
} }
} }

View File

@@ -0,0 +1,55 @@
package ru.ua1zbe.aprsmap
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import ru.rustore.sdk.pushclient.messaging.exception.RuStorePushClientException
import ru.rustore.sdk.pushclient.messaging.model.RemoteMessage
import ru.rustore.sdk.pushclient.messaging.service.RuStoreMessagingService
class PushMessagingService : RuStoreMessagingService() {
override fun onNewToken(token: String) {
Log.i(TAG, "onNewToken: $token")
getSharedPreferences(App.PREFS, Context.MODE_PRIVATE)
.edit().putString("push_token", token).apply()
}
override fun onMessageReceived(message: RemoteMessage) {
if (message.notification != null) {
Log.i(TAG, "push displayed by SDK: ${message.data}")
return
}
val title = message.data["title"] ?: getString(R.string.app_name)
val body = message.data["body"] ?: ""
if (title.isNotEmpty() || body.isNotEmpty()) {
showPush(title, body)
}
}
override fun onDeletedMessages() {
Log.i(TAG, "onDeletedMessages")
}
override fun onError(errors: List<RuStorePushClientException>) {
errors.forEach { Log.w(TAG, "error", it) }
}
private fun showPush(title: String, body: String) {
val nm = getSystemService(NotificationManager::class.java)
val n = Notification.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setAutoCancel(true)
.build()
nm.notify(NOTIFICATION_ID, n)
}
companion object {
private const val TAG = "RuStorePush"
const val CHANNEL_ID = "aprs_push"
private const val NOTIFICATION_ID = 2
}
}

View File

@@ -11,6 +11,7 @@ class SplashActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
setContentView(SplashView(this)) setContentView(SplashView(this))

View File

@@ -2,6 +2,8 @@
<resources> <resources>
<string name="app_name">APRS RadioRubka</string> <string name="app_name">APRS RadioRubka</string>
<string name="map_url">https://aprs.radiorubka.org/?center=67.5096,32.8271&amp;zoom=8</string> <string name="map_url">https://aprs.radiorubka.org/?center=67.5096,32.8271&amp;zoom=8</string>
<string name="push_channel_id">aprs_push</string>
<string name="push_channel_name">Пуш-уведомления</string>
<string name="update_available_title">Доступно обновление</string> <string name="update_available_title">Доступно обновление</string>
<string name="update_available_msg">Версия %1$s уже вышла. Обновить приложение?</string> <string name="update_available_msg">Версия %1$s уже вышла. Обновить приложение?</string>
<string name="update_btn">Обновить</string> <string name="update_btn">Обновить</string>

View File

@@ -6,4 +6,4 @@ org.gradle.parallel=false
org.gradle.caching=false org.gradle.caching=false
kotlin.compiler.execution.strategy=in-process kotlin.compiler.execution.strategy=in-process
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.useAndroidX=false android.useAndroidX=true

View File

@@ -11,6 +11,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven("https://nexus-external.rustore.ru/repository/maven-rustore-exposed/")
} }
} }

23
tools/push.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Рассылка пуша всем подписчикам топика "aprsmap" (RuStore Push).
# Использование: ./push.sh "Заголовок" "Текст"
# project_id и service token читаются из tools/.push.secrets (в git не хранится).
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ -f "$DIR/.push.secrets" ] && . "$DIR/.push.secrets"
SERVICE_TOKEN="${SERVICE_TOKEN:-${SSP:-}}"
PROJECT_ID="${PROJECT_ID:-}"
[ -n "$SERVICE_TOKEN" ] || { echo "SERVICE_TOKEN is not set" >&2; exit 1; }
[ -n "$PROJECT_ID" ] || { echo "PROJECT_ID is not set" >&2; exit 1; }
title="${1:?usage: push.sh <title> <body>}"
body="${2:-}"
curl -s -X POST \
"https://vkpns-topics.rustore.ru/v1/projects/${PROJECT_ID}/topics/aprsmap/publish" \
-H "Authorization: Bearer ${SERVICE_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"message\":{\"notification\":{\"title\":\"${title}\",\"body\":\"${body}\"}}}"
echo