Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddc9cf2584 | |||
| a8ec1ce654 | |||
| 187de60f5c | |||
| c7a7726eb7 | |||
| ccbde3a9dd | |||
| 828106a6fc | |||
| e7df71281d | |||
| dbad63bc77 | |||
| af1f215b8a | |||
| 38adfdb34b | |||
| 5e15e685fa | |||
| baabcd31f8 | |||
| 83fc10878b | |||
| 576f794ff7 | |||
| bd1db2a468 | |||
| 905214b49e | |||
| 74d3f395ce | |||
| 034e7e1e27 | |||
| 09bffa78a3 | |||
| f0088b08b6 | |||
| 91a43a37b4 | |||
| 4a2aa59c77 | |||
| 3aab0b826b | |||
| 6e03bc997c | |||
| f04bb1843c |
72
README.md
72
README.md
@@ -12,6 +12,9 @@
|
||||
- **Автоопределение дня недели** — будни/выходные.
|
||||
- **Полное расписание** — по кнопке раскрывается вся таблица
|
||||
(4 остановки: Кандалакша · ПЗ→Зашеек · Зашеек · ПЗ→Кандалакша).
|
||||
- **Пуш-уведомления** — через RuStore Push, см. раздел ниже.
|
||||
- **Меню** — кнопка `⋮` в шапке открывает выпадающее окошко с пунктом
|
||||
«О приложении» (описание и версия сборки).
|
||||
- Тёмная тема, живые часы МСК, подсветка ближайших рейсов (< 15 мин).
|
||||
|
||||
## Технические данные
|
||||
@@ -24,6 +27,65 @@
|
||||
| Сборка | Gradle 8.6 + AGP 8.2.2 |
|
||||
| Подпись | keystore `~/bus127.keystore` (alias `bus127`) |
|
||||
|
||||
## Пуш-уведомления (RuStore)
|
||||
|
||||
Приложение интегрировано с [RuStore Push SDK](https://www.rustore.ru/help/sdk/push-notifications)
|
||||
(аналог Firebase Cloud Messaging). Проект в консоли RuStore привязан к пакету
|
||||
`ru.ua1zbe.bus127` и подписи release-keystore; пока подпись не меняется, перерегистрация не нужна.
|
||||
|
||||
Для доставки на устройстве должен быть установлен и авторизован **RuStore**.
|
||||
|
||||
Рассылка выполняется с сервера через API RuStore:
|
||||
|
||||
```
|
||||
POST https://vkpns.rustore.ru/v1/projects/{project_id}/messages:send
|
||||
Authorization: Bearer {service-token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": {
|
||||
"token": "{push-token устройства}",
|
||||
"notification": {
|
||||
"title": "Автобус 127",
|
||||
"body": "Ближайший рейс через 5 мин"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `project_id` и `service-token` берутся в консоли RuStore: **Push-уведомления > Проекты**.
|
||||
В приложение попадает только `project_id`; сервисный токен в APK не хранится.
|
||||
- `push-token` устройства приходит в `onNewToken` (лог `RuStorePush`) и сохраняется
|
||||
в `SharedPreferences` `bus127_push`.
|
||||
- Приложение подписывается на топик `bus127` при старте. Отдельного создания топика
|
||||
в консоли не требуется — публикация в него проходит и для несуществующего.
|
||||
- Если в запросе есть секция `notification` — уведомление показывает сам SDK;
|
||||
если переданы только `data` (`title`/`body`, необязательный `url`) — приложение
|
||||
показывает его само и открывает ссылку по клику.
|
||||
|
||||
### Рассылка
|
||||
|
||||
**Всем подписчикам топика** (все устройства, открывшие приложение):
|
||||
|
||||
```
|
||||
POST https://vkpns-topics.rustore.ru/v1/projects/{project_id}/topics/{topic}/publish
|
||||
Authorization: Bearer {service-token}
|
||||
{"message":{"notification":{"title":"...","body":"..."}}}
|
||||
```
|
||||
|
||||
**Одному устройству** — по его push-токену:
|
||||
|
||||
```
|
||||
POST https://vkpns.rustore.ru/v1/projects/{project_id}/messages:send
|
||||
Authorization: Bearer {service-token}
|
||||
{"message":{"token":"...","notification":{"title":"...","body":"..."}}}
|
||||
```
|
||||
|
||||
Если устройства не подписались сами, их токены можно подписать разом:
|
||||
`POST .../topics/{topic}/batch_subscribe` с `{"push_tokens":[...]}`.
|
||||
Ответ `400` с текстом про `registration token` означает, что адресат не указан
|
||||
или токен устройства неверен; `401`/`403` — неверный сервисный токен или его пара с `project_id`.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
@@ -32,7 +94,15 @@ apk-webview/ # исходники APK (WebView)
|
||||
│ ├── build.gradle
|
||||
│ └── src/main/
|
||||
│ ├── AndroidManifest.xml
|
||||
│ ├── java/ru/ua1zbe/bus127/MainActivity.java
|
||||
│ ├── java/ru/ua1zbe/bus127/
|
||||
│ │ ├── MainActivity.java # главный экран: WebView + запрос разрешения
|
||||
│ │ ├── AboutActivity.java # окно «О приложении»
|
||||
│ │ ├── Webs.java # общий WebView для всех экранов
|
||||
│ │ ├── BusBridge.java # мост страницы и приложения (меню, назад, версия)
|
||||
│ │ ├── Bus127App.java # инициализация RuStore Push SDK
|
||||
│ │ ├── PushMessagingService.java # приём уведомлений
|
||||
│ │ ├── PushIntents.java # переход по клику
|
||||
│ │ └── push/PushStore.java # хранение push-токена
|
||||
│ ├── assets/index.html # интерфейс (HTML/JS)
|
||||
│ └── res/values/strings.xml
|
||||
└── settings.gradle, build.gradle, gradle.properties
|
||||
|
||||
@@ -19,8 +19,8 @@ android {
|
||||
applicationId "ru.ua1zbe.bus127"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 8
|
||||
versionName "2.6"
|
||||
versionCode 12
|
||||
versionName "2.6.3"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -35,3 +35,7 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "ru.rustore.sdk:pushclient:7.4.0"
|
||||
}
|
||||
|
||||
@@ -1,22 +1,51 @@
|
||||
<?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.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".Bus127App"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar">
|
||||
|
||||
<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="@mipmap/ic_launcher" />
|
||||
<meta-data
|
||||
android:name="ru.rustore.sdk.pushclient.default_notification_channel_id"
|
||||
android:value="@string/push_channel_id" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/about_title"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -55,6 +55,44 @@
|
||||
.fullbtn:hover{border-color:var(--accent2)}
|
||||
.fullbtn .chev{float:right;transition:.2s;color:var(--muted)}
|
||||
.fullbtn.open .chev{transform:rotate(180deg)}
|
||||
.fulltitle{display:none;margin:0;padding:14px 12px 2px;font-size:16px;font-weight:700}
|
||||
|
||||
/* строка заголовка: стрелка назад слева, меню справа, название ровно по центру */
|
||||
.headwrap{position:relative}
|
||||
.headrow{display:grid;grid-template-columns:38px 1fr 38px;align-items:center;gap:8px}
|
||||
.headrow h1{text-align:center;margin:0;min-width:0}
|
||||
.iconbtn{display:none;background:var(--card2);border:1px solid var(--line);
|
||||
color:var(--txt);border-radius:10px;width:38px;height:38px;
|
||||
font-size:22px;line-height:1;font-family:inherit;cursor:pointer;
|
||||
align-items:center;justify-content:center;padding:0}
|
||||
body.app .iconbtn,body.about .iconbtn{display:inline-flex}
|
||||
/* на главном экране стрелки нет, но место под неё оставляем, иначе съедет центр */
|
||||
body.app .iconbtn.back{visibility:hidden}
|
||||
/* троеточие в круглой кнопке приподнято: рамка остаётся на одной линии с названием */
|
||||
#navMenu{padding-bottom:7px}
|
||||
.iconbtn:active{border-color:var(--accent2)}
|
||||
|
||||
/* выпадающее окошко: висит поверх содержимого, прижато к кнопке меню */
|
||||
.menu{position:absolute;top:calc(100% + 8px);right:0;z-index:60;
|
||||
width:min(260px,100%);background:var(--card);border:1px solid var(--line);
|
||||
border-radius:14px;overflow:hidden;margin:0;
|
||||
box-shadow:0 18px 44px rgba(0,0,0,.6)}
|
||||
.menu[hidden]{display:none}
|
||||
.menuitem{display:block;width:100%;text-align:left;padding:13px 14px;border:0;
|
||||
background:transparent;color:var(--txt);font-size:15px;font-weight:600;
|
||||
font-family:inherit;cursor:pointer}
|
||||
.menuitem:hover{background:var(--card2)}
|
||||
|
||||
/* экран «О приложении» */
|
||||
.aboutcard{display:none;background:var(--card);border:1px solid var(--line);
|
||||
border-radius:14px;padding:16px;margin-bottom:10px}
|
||||
body.about .aboutcard{display:block}
|
||||
body.about .pickers,body.about .where,body.about .dir,
|
||||
body.about #fullTable,body.about .fullbtn{display:none}
|
||||
.aboutcard h2{margin:0 0 4px;font-size:17px}
|
||||
.aboutcard .ver{color:var(--muted);font-size:14px;margin-bottom:12px}
|
||||
.aboutcard p{margin:0 0 10px;font-size:14px;line-height:1.5}
|
||||
.aboutcard p.muted{color:var(--muted);font-size:13px}
|
||||
|
||||
.tablecard{background:var(--card);border:1px solid var(--line);border-radius:14px;
|
||||
overflow:hidden;display:none;margin-bottom:8px}
|
||||
@@ -85,7 +123,17 @@
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="headwrap">
|
||||
<div class="headrow">
|
||||
<button class="iconbtn back" id="navBack" aria-label="Назад">‹</button>
|
||||
<h1>Автобус №127</h1>
|
||||
<button class="iconbtn" id="navMenu" aria-label="Меню">⋮</button>
|
||||
</div>
|
||||
|
||||
<nav class="menu" id="menuBox" hidden>
|
||||
<button class="menuitem" id="mAbout">О приложении</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="sub">Кандалакша — Полярные Зори — Зашеек</div>
|
||||
<div class="clock" id="clock">--:--:-- <small>МСК</small></div>
|
||||
<div class="day" id="dayBadge"></div>
|
||||
@@ -103,6 +151,7 @@
|
||||
|
||||
<button class="fullbtn" id="fullBtn">Полное расписание <span class="chev">▾</span></button>
|
||||
<section class="tablecard" id="fullTable">
|
||||
<h2 class="fulltitle">Полное расписание</h2>
|
||||
<div class="days" id="dayPick">
|
||||
<button data-day="weekday" class="active">Будни</button>
|
||||
<button data-day="saturday">Суббота</button>
|
||||
@@ -117,6 +166,18 @@
|
||||
<div id="tableBody"></div>
|
||||
</section>
|
||||
|
||||
<section class="aboutcard" id="aboutBox">
|
||||
<h2>О приложении</h2>
|
||||
<div class="ver" id="aboutVer"></div>
|
||||
<p>Расписание автобуса №127 по маршруту Кандалакша — Полярные Зори — Зашеек.</p>
|
||||
<p>Ближайшие рейсы считаются по московскому времени и обновляются каждые 30 секунд.
|
||||
Полное расписание раскрывается прямо на этом экране — кнопкой под списком рейсов,
|
||||
с переключением между буднями, субботой и воскресеньем.</p>
|
||||
<p class="muted">Расписание действует с 30.09.2020. Время московское (МСК).</p>
|
||||
<p class="muted">Пуш-уведомления приходят через RuStore Push — для этого нужен
|
||||
установленный и авторизованный RuStore на устройстве.</p>
|
||||
</section>
|
||||
|
||||
<footer>Расписание действует с 30.09.2020 · время московское (МСК)</footer>
|
||||
</div>
|
||||
|
||||
@@ -255,13 +316,17 @@ let currentStop = "Полярные Зори";
|
||||
// Date#getDay(): 0 — воскресенье, 6 — суббота
|
||||
function dayKeyFor(dow){ return dow===0 ? "sunday" : dow===6 ? "saturday" : "weekday"; }
|
||||
|
||||
// Карточки ближайших рейсов всегда показывают реальный сегодняшний день
|
||||
function todayKey(){ return dayKeyFor(nowMs().day); }
|
||||
|
||||
// Выбор пользователя — только для полного расписания
|
||||
function sched(){ return SCHEDULES[activeDay]; }
|
||||
|
||||
function render(){
|
||||
const now = nowMs();
|
||||
const curMin = now.hour*60 + now.minute;
|
||||
const dirs = DIRECTIONS[currentStop];
|
||||
const data = sched();
|
||||
const data = SCHEDULES[todayKey()];
|
||||
document.getElementById("dirBox").innerHTML = dirs.map(d=>{
|
||||
const upcoming = data
|
||||
.map((r,ri)=>{
|
||||
@@ -303,12 +368,12 @@ function renderDay(){
|
||||
document.getElementById("dayBadge").innerHTML = `Сегодня <b>${DAYS[dow]}</b>`;
|
||||
}
|
||||
|
||||
// Подсветка выбранного типа дня и перерисовка расписания
|
||||
// Подсветка выбранного типа дня. Таблица полного расписания перерисовывается,
|
||||
// карточки ближайших рейсов не трогаем — они всегда на сегодня.
|
||||
function selectDay(key){
|
||||
activeDay = key;
|
||||
document.querySelectorAll("#dayPick button").forEach(b=>
|
||||
b.classList.toggle("active", b.dataset.day === key));
|
||||
render();
|
||||
renderTable();
|
||||
}
|
||||
|
||||
@@ -342,9 +407,38 @@ document.getElementById("fullBtn").addEventListener("click",()=>{
|
||||
b.classList.toggle("open", !open);
|
||||
});
|
||||
|
||||
let activeDay = dayKeyFor(nowMs().day); // по умолчанию — реальный день недели
|
||||
// Режим показа задаётся якорью в адресе:
|
||||
// (пусто) — браузер;
|
||||
// #app — главный экран приложения;
|
||||
// #about — окно «О приложении».
|
||||
// В приложении (не в браузере) страница дёргает нативный код через объект Bus.
|
||||
const MODE = location.hash.replace("#","") || "web";
|
||||
const NATIVE = MODE !== "web" && typeof Bus !== "undefined";
|
||||
|
||||
document.body.classList.add(MODE);
|
||||
if (MODE === "about" && NATIVE) {
|
||||
// версию берём из приложения, чтобы не расходилась со сборкой
|
||||
const v = document.getElementById("aboutVer");
|
||||
if (Bus.version && Bus.version()) v.textContent = "Версия " + Bus.version();
|
||||
}
|
||||
|
||||
if (NATIVE) {
|
||||
const menuBox = document.getElementById("menuBox");
|
||||
const closeMenu = () => { menuBox.hidden = true; };
|
||||
document.getElementById("navMenu").addEventListener("click",e=>{
|
||||
e.stopPropagation();
|
||||
menuBox.hidden = !menuBox.hidden;
|
||||
});
|
||||
document.getElementById("navBack").addEventListener("click",()=>Bus.goBack());
|
||||
document.getElementById("mAbout").addEventListener("click",()=>Bus.openAbout());
|
||||
document.addEventListener("click",e=>{
|
||||
if (!e.target.closest(".menu") && !e.target.closest("#navMenu")) closeMenu();
|
||||
});
|
||||
}
|
||||
|
||||
let activeDay = todayKey(); // по умолчанию — реальный день недели
|
||||
selectDay(activeDay);
|
||||
tickClock(); renderDay();
|
||||
tickClock(); renderDay(); render();
|
||||
setInterval(tickClock,1000);
|
||||
setInterval(render,30000);
|
||||
setInterval(renderDay,60000);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Описание приложения и версия. Рисуется той же страницей расписания,
|
||||
* поэтому тема оформления совпадает с остальными экранами.
|
||||
*/
|
||||
public class AboutActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(Webs.create(this, Webs.URL_ABOUT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.util.Log;
|
||||
|
||||
import ru.rustore.sdk.pushclient.RuStorePushClient;
|
||||
import ru.ua1zbe.bus127.push.PushStore;
|
||||
|
||||
/**
|
||||
* Инициализация RuStore Push SDK.
|
||||
*
|
||||
* project_id и topic берутся из консоли RuStore (Push-уведомления > Проекты).
|
||||
* Сервисный токен в приложении не хранится — он нужен только серверу
|
||||
* для вызова https://vkpns.rustore.ru/v1/projects/{project_id}/messages:send
|
||||
*/
|
||||
public class Bus127App extends Application {
|
||||
|
||||
public static final String CHANNEL_ID = "bus127_push";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createPushChannel();
|
||||
initRuStorePush();
|
||||
}
|
||||
|
||||
private void createPushChannel() {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
nm.createNotificationChannel(new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.push_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH));
|
||||
}
|
||||
|
||||
private void initRuStorePush() {
|
||||
try {
|
||||
RuStorePushClient.INSTANCE.init(this, RUSTORE_PROJECT_ID);
|
||||
RuStorePushClient.INSTANCE.getToken()
|
||||
.addOnSuccessListener(token -> {
|
||||
Log.i(TAG, "push token: " + token);
|
||||
PushStore.setToken(this, token);
|
||||
RuStorePushClient.INSTANCE.subscribeToTopic(PUSH_TOPIC)
|
||||
.addOnSuccessListener(v ->
|
||||
Log.i(TAG, "subscribed to " + PUSH_TOPIC))
|
||||
.addOnFailureListener(e ->
|
||||
Log.w(TAG, "subscribe to " + PUSH_TOPIC + " failed", e));
|
||||
})
|
||||
.addOnFailureListener(e ->
|
||||
Log.w(TAG, "getToken failed", e));
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "init failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TAG = "RuStorePush";
|
||||
private static final String RUSTORE_PROJECT_ID = "upSEOtZyXP96tskl4hf-442AqciCUTxs";
|
||||
private static final String PUSH_TOPIC = "bus127";
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.webkit.JavascriptInterface;
|
||||
|
||||
/**
|
||||
* Мост между страницей расписания и приложением: кнопки заголовка
|
||||
* (стрелка назад и меню) вызывают сюда. В браузерной версии страницы
|
||||
* объект не существует, и кнопки там не показываются.
|
||||
*/
|
||||
public class BusBridge {
|
||||
|
||||
private final Activity activity;
|
||||
|
||||
BusBridge(Activity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void goBack() {
|
||||
activity.runOnUiThread(activity::finish);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void openAbout() {
|
||||
open(AboutActivity.class);
|
||||
}
|
||||
|
||||
/** «2.6.3 (сборка 12)» — чтобы текст не расходился со сборкой. */
|
||||
@JavascriptInterface
|
||||
public String version() {
|
||||
try {
|
||||
PackageInfo info = activity.getPackageManager()
|
||||
.getPackageInfo(activity.getPackageName(), 0);
|
||||
long code = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
? info.getLongVersionCode() : info.versionCode;
|
||||
return info.versionName + " (сборка " + code + ")";
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void open(Class<?> target) {
|
||||
activity.runOnUiThread(() ->
|
||||
activity.startActivity(new Intent(activity, target)));
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package ru.ua1zbe.bus127;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.WindowManager;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
@Override
|
||||
@@ -13,13 +10,16 @@ public class MainActivity extends Activity {
|
||||
super.onCreate(savedInstanceState);
|
||||
// экран не гаснет, пока открыто расписание
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
WebView webView = new WebView(this);
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setSupportZoom(false);
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
webView.loadUrl("file:///android_asset/index.html");
|
||||
setContentView(webView);
|
||||
requestNotificationsPermission();
|
||||
setContentView(Webs.create(this, Webs.URL_MAIN));
|
||||
}
|
||||
|
||||
// Android 13+ требует явного согласия на показ уведомлений
|
||||
private void requestNotificationsPermission() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU
|
||||
&& checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
/** Открытие расписания по клику на уведомление. */
|
||||
final class PushIntents {
|
||||
|
||||
private PushIntents() {
|
||||
}
|
||||
|
||||
static PendingIntent web(Context context, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
return PendingIntent.getActivity(context, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.rustore.sdk.pushclient.messaging.exception.RuStorePushClientException;
|
||||
import ru.rustore.sdk.pushclient.messaging.model.RemoteMessage;
|
||||
import ru.rustore.sdk.pushclient.messaging.service.RuStoreMessagingService;
|
||||
import ru.ua1zbe.bus127.push.PushStore;
|
||||
|
||||
/**
|
||||
* Приём пуш-уведомлений RuStore.
|
||||
*
|
||||
* Если сервер прислал секцию notification — SDK показывает уведомление сам.
|
||||
* Если пришли только data (title/body) — показываем уведомление сами.
|
||||
*/
|
||||
public class PushMessagingService extends RuStoreMessagingService {
|
||||
|
||||
@Override
|
||||
public void onNewToken(String token) {
|
||||
Log.i(TAG, "onNewToken: " + token);
|
||||
PushStore.setToken(this, token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage message) {
|
||||
if (message.getNotification() != null) {
|
||||
Log.i(TAG, "push displayed by SDK: " + message.getData());
|
||||
return;
|
||||
}
|
||||
Map<String, String> data = message.getData();
|
||||
String title = data.get("title");
|
||||
if (title == null || title.isEmpty()) {
|
||||
title = getString(R.string.app_name);
|
||||
}
|
||||
String body = data.get("body");
|
||||
if (body == null) {
|
||||
body = "";
|
||||
}
|
||||
if (!title.isEmpty() || !body.isEmpty()) {
|
||||
showPush(title, body, data.get("url"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeletedMessages() {
|
||||
Log.i(TAG, "onDeletedMessages");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(List<? extends RuStorePushClientException> errors) {
|
||||
for (RuStorePushClientException e : errors) {
|
||||
Log.w(TAG, "error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void showPush(String title, String body, String url) {
|
||||
Notification.Builder b = new Notification.Builder(this, Bus127App.CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setAutoCancel(true);
|
||||
if (url != null && !url.isEmpty()) {
|
||||
b.setContentIntent(PushIntents.web(this, url));
|
||||
}
|
||||
getSystemService(NotificationManager.class).notify(NOTIFICATION_ID, b.build());
|
||||
}
|
||||
|
||||
private static final String TAG = "RuStorePush";
|
||||
private static final int NOTIFICATION_ID = 127;
|
||||
}
|
||||
30
apk-webview/app/src/main/java/ru/ua1zbe/bus127/Webs.java
Normal file
30
apk-webview/app/src/main/java/ru/ua1zbe/bus127/Webs.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package ru.ua1zbe.bus127;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
/** Создание WebView с одинаковыми настройками для всех экранов. */
|
||||
final class Webs {
|
||||
|
||||
/** Главный экран: полное расписание раскрывается прямо на странице. */
|
||||
static final String URL_MAIN = "file:///android_asset/index.html#app";
|
||||
/** Описание приложения и версия. */
|
||||
static final String URL_ABOUT = "file:///android_asset/index.html#about";
|
||||
|
||||
private Webs() {
|
||||
}
|
||||
|
||||
static WebView create(Activity activity, String url) {
|
||||
WebView webView = new WebView(activity);
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setSupportZoom(false);
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
webView.addJavascriptInterface(new BusBridge(activity), "Bus");
|
||||
webView.loadUrl(url);
|
||||
return webView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package ru.ua1zbe.bus127.push;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/** Хранение push-токена устройства, чтобы отправлять ему уведомления с сервера. */
|
||||
public final class PushStore {
|
||||
|
||||
private static final String PREFS = "bus127_push";
|
||||
private static final String KEY_TOKEN = "push_token";
|
||||
|
||||
private PushStore() {
|
||||
}
|
||||
|
||||
private static SharedPreferences sp(Context context) {
|
||||
return context.getApplicationContext()
|
||||
.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static String getToken(Context context) {
|
||||
return sp(context).getString(KEY_TOKEN, "");
|
||||
}
|
||||
|
||||
public static void setToken(Context context, String token) {
|
||||
sp(context).edit().putString(KEY_TOKEN, token).apply();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Автобус 127</string>
|
||||
<string name="push_channel_id">bus127_push</string>
|
||||
<string name="push_channel_name">Пуш-уведомления</string>
|
||||
|
||||
<string name="about_title">О приложении</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,7 +2,11 @@ pluginManagement {
|
||||
repositories { google(); mavenCentral(); gradlePluginPortal() }
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositories { google(); mavenCentral() }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://nexus-external.rustore.ru/repository/maven-rustore-exposed/" }
|
||||
}
|
||||
}
|
||||
rootProject.name = "BusSchedule127"
|
||||
include ':app'
|
||||
|
||||
@@ -255,13 +255,17 @@ let currentStop = "Полярные Зори";
|
||||
// Date#getDay(): 0 — воскресенье, 6 — суббота
|
||||
function dayKeyFor(dow){ return dow===0 ? "sunday" : dow===6 ? "saturday" : "weekday"; }
|
||||
|
||||
// Карточки ближайших рейсов всегда показывают реальный сегодняшний день
|
||||
function todayKey(){ return dayKeyFor(nowMs().day); }
|
||||
|
||||
// Выбор пользователя — только для полного расписания
|
||||
function sched(){ return SCHEDULES[activeDay]; }
|
||||
|
||||
function render(){
|
||||
const now = nowMs();
|
||||
const curMin = now.hour*60 + now.minute;
|
||||
const dirs = DIRECTIONS[currentStop];
|
||||
const data = sched();
|
||||
const data = SCHEDULES[todayKey()];
|
||||
document.getElementById("dirBox").innerHTML = dirs.map(d=>{
|
||||
const upcoming = data
|
||||
.map((r,ri)=>{
|
||||
@@ -303,12 +307,12 @@ function renderDay(){
|
||||
document.getElementById("dayBadge").innerHTML = `Сегодня <b>${DAYS[dow]}</b>`;
|
||||
}
|
||||
|
||||
// Подсветка выбранного типа дня и перерисовка расписания
|
||||
// Подсветка выбранного типа дня. Таблица полного расписания перерисовывается,
|
||||
// карточки ближайших рейсов не трогаем — они всегда на сегодня.
|
||||
function selectDay(key){
|
||||
activeDay = key;
|
||||
document.querySelectorAll("#dayPick button").forEach(b=>
|
||||
b.classList.toggle("active", b.dataset.day === key));
|
||||
render();
|
||||
renderTable();
|
||||
}
|
||||
|
||||
@@ -342,9 +346,9 @@ document.getElementById("fullBtn").addEventListener("click",()=>{
|
||||
b.classList.toggle("open", !open);
|
||||
});
|
||||
|
||||
let activeDay = dayKeyFor(nowMs().day); // по умолчанию — реальный день недели
|
||||
let activeDay = todayKey(); // по умолчанию — реальный день недели
|
||||
selectDay(activeDay);
|
||||
tickClock(); renderDay();
|
||||
tickClock(); renderDay(); render();
|
||||
setInterval(tickClock,1000);
|
||||
setInterval(render,30000);
|
||||
setInterval(renderDay,60000);
|
||||
|
||||
Reference in New Issue
Block a user