Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ android {
ndkVersion rootProject.ext.ndkVersion

buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
// Android 16 (API 36) の Notification.ProgressStyle を使用するため最低 36 が必要
compileSdk Math.max(rootProject.ext.compileSdkVersion as int, 36)

namespace 'me.tinykitten.trainlcd'
defaultConfig {
Expand Down
152 changes: 152 additions & 0 deletions android/app/src/main/java/me/tinykitten/trainlcd/LiveUpdateModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package me.tinykitten.trainlcd

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.Build
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap

class LiveUpdateModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

companion object {
private const val CHANNEL_ID = "live_update"
private const val NOTIFICATION_ID = 49152
private const val MAX_PROGRESS = 1000
}

override fun getName() = "LiveUpdateModule"

private fun getNotificationManager(): NotificationManager =
reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

private fun ensureChannel() {
val nm = getNotificationManager()
if (nm.getNotificationChannel(CHANNEL_ID) != null) return

val channel = NotificationChannel(
CHANNEL_ID,
"運行情報",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "現在の運行状況をリアルタイムで表示します"
setShowBadge(false)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
nm.createNotificationChannel(channel)
}

private fun createContentIntent(): PendingIntent {
val intent = reactApplicationContext.packageManager
.getLaunchIntentForPackage(reactApplicationContext.packageName)
return PendingIntent.getActivity(
reactApplicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}

@ReactMethod
fun startLiveUpdate(state: ReadableMap) {
if (Build.VERSION.SDK_INT < 36) return
postProgressNotification(state)
}

@ReactMethod
fun updateLiveUpdate(state: ReadableMap) {
if (Build.VERSION.SDK_INT < 36) return
postProgressNotification(state)
}

@ReactMethod
fun stopLiveUpdate() {
getNotificationManager().cancel(NOTIFICATION_ID)
}

@Suppress("NewApi")
private fun postProgressNotification(state: ReadableMap) {
ensureChannel()

val stationName = state.getString("stationName") ?: ""
val nextStationName = state.getString("nextStationName") ?: ""
val approaching = state.getBoolean("approaching")
val stopped = state.getBoolean("stopped")
val lineName = state.getString("lineName") ?: ""
val lineColor = state.getString("lineColor") ?: "#000000"
val progress = state.getDouble("progress")
val trainTypeName = state.getString("trainTypeName") ?: ""
val boundStationName = state.getString("boundStationName") ?: ""
val passingStationName = state.getString("passingStationName") ?: ""

val parsedColor = try {
Color.parseColor(lineColor)
} catch (_: Exception) {
Color.BLACK
}

val progressInt = (progress * MAX_PROGRESS).toInt().coerceIn(0, MAX_PROGRESS)

val contentTitle = when {
passingStationName.isNotEmpty() -> "$passingStationName 通過中"
stopped -> stationName
approaching -> "まもなく $nextStationName"
else -> "$stationName → $nextStationName"
}

val contentText = if (boundStationName.isNotEmpty()) {
"${boundStationName}方面"
} else {
""
}

val subText = buildString {
if (trainTypeName.isNotEmpty()) {
append(trainTypeName)
append(" ")
}
append(lineName)
}

val trackerIcon = Icon.createWithResource(
reactApplicationContext,
R.drawable.ic_notification_live_update
)

val progressStyle = Notification.ProgressStyle()
.setStyledByProgress(true)
.setProgress(progressInt)
.setProgressTrackerIcon(trackerIcon)
.setProgressSegments(
listOf(
Notification.ProgressStyle.Segment(MAX_PROGRESS).setColor(parsedColor)
)
)
.setProgressPoints(
listOf(
Notification.ProgressStyle.Point(0).setColor(parsedColor),
Notification.ProgressStyle.Point(MAX_PROGRESS).setColor(parsedColor)
)
)

val notification = Notification.Builder(reactApplicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_live_update)
.setContentTitle(contentTitle)
.setContentText(contentText)
.setSubText(subText)
.setStyle(progressStyle)
.setContentIntent(createContentIntent())
.setOngoing(true)
.setOnlyAlertOnce(true)
.build()

getNotificationManager().notify(NOTIFICATION_ID, notification)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class TrainLCDPackage : ReactPackage {
): MutableList<NativeModule> = listOf(
WearableModule(reactContext),
IgnoreBatteryOptimizationsModule(reactContext),
GnssModule(reactContext)
GnssModule(reactContext),
LiveUpdateModule(reactContext)
).toMutableList()
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2c-4,0 -8,0.5 -8,4v9.5C4,17.43 5.57,19 7.5,19L6,20.5v0.5h2l2,-2h4l2,2h2v-0.5L16.5,19c1.93,0 3.5,-1.57 3.5,-3.5V6c0,-3.5 -3.58,-4 -8,-4zM7.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5S6.67,14 7.5,14s1.5,0.67 1.5,1.5S8.33,17 7.5,17zM11,10L6,10V6h5v4zM13,10v-4h5v4h-5zM16.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>
3 changes: 3 additions & 0 deletions src/constants/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ import { Platform } from 'react-native';

export const IS_LIVE_ACTIVITIES_ELIGIBLE_PLATFORM =
Platform.OS === 'ios' && Number.parseFloat(Platform.Version) >= 16.1;

export const IS_LIVE_UPDATE_ELIGIBLE_PLATFORM =
Platform.OS === 'android' && (Platform.Version as number) >= 36;
8 changes: 8 additions & 0 deletions src/hooks/useUpdateLiveActivities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { directionToDirectionName } from '../models/Bound';
import stationState from '../store/atoms/station';
import { isJapanese } from '../translation';
import getIsPass from '../utils/isPass';
import {
startLiveUpdate,
stopLiveUpdate,
updateLiveUpdate,
} from '../utils/native/android/liveUpdateModule';
import {
startLiveActivity,
stopLiveActivity,
Expand Down Expand Up @@ -236,20 +241,23 @@ export const useUpdateLiveActivities = (): void => {
useEffect(() => {
if (selectedBound && !started) {
startLiveActivity(activityState);
startLiveUpdate(activityState);
setStarted(true);
}
}, [activityState, selectedBound, started]);

useEffect(() => {
return () => {
stopLiveActivity();
stopLiveUpdate();
setStarted(false);
};
}, []);

useEffect(() => {
if (started) {
updateLiveActivity(activityState);
updateLiveUpdate(activityState);
}
}, [activityState, started]);
};
38 changes: 38 additions & 0 deletions src/utils/native/android/liveUpdateModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NativeModules } from 'react-native';
import { IS_LIVE_UPDATE_ELIGIBLE_PLATFORM } from '../../../constants';

const { LiveUpdateModule } = NativeModules;

type LiveUpdateState = {
stationName: string;
nextStationName: string;
stationNumber: string;
nextStationNumber: string;
approaching: boolean;
stopped: boolean;
lineName: string;
lineColor: string;
passingStationName: string;
passingStationNumber: string;
progress: number;
boundStationName: string;
trainTypeName: string;
};

export const startLiveUpdate = (state?: LiveUpdateState) => {
if (IS_LIVE_UPDATE_ELIGIBLE_PLATFORM) {
LiveUpdateModule?.startLiveUpdate?.(state);
}
};

export const updateLiveUpdate = (state: LiveUpdateState) => {
if (IS_LIVE_UPDATE_ELIGIBLE_PLATFORM) {
LiveUpdateModule?.updateLiveUpdate?.(state);
}
};

export const stopLiveUpdate = () => {
if (IS_LIVE_UPDATE_ELIGIBLE_PLATFORM) {
LiveUpdateModule?.stopLiveUpdate?.();
}
};