mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
feat(android): add background time tracking via foreground service
When a task is being tracked and the app goes to the background on Android, JavaScript execution is paused/throttled, causing time tracking to stop. This adds a native Foreground Service that: - Shows a persistent notification with task title and elapsed time - Tracks time independently using timestamps (not JS ticks) - Provides Pause and Done action buttons on the notification - Syncs elapsed time back to Angular when the app resumes or task changes Architecture: - Native service stores startTimestamp + accumulatedMs - Calculates elapsed = (now - startTimestamp) + accumulated - Angular effects sync time on resume, task switch, and notification actions
This commit is contained in:
parent
afa6bb85ea
commit
ffa7122aea
8 changed files with 518 additions and 7 deletions
|
|
@ -8,6 +8,7 @@
|
|||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<application
|
||||
|
|
@ -76,5 +77,15 @@
|
|||
android:name=".webview.WebViewBlockActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<service
|
||||
android:name=".service.TrackingForegroundService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="time_tracking" />
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.activity.addCallback
|
|||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.getcapacitor.BridgeActivity
|
||||
import com.superproductivity.superproductivity.plugins.SafBridgePlugin
|
||||
import com.superproductivity.superproductivity.service.TrackingForegroundService
|
||||
import com.superproductivity.superproductivity.util.printWebViewVersion
|
||||
import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
||||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
|
|
@ -144,6 +145,22 @@ class CapacitorMainActivity : BridgeActivity() {
|
|||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
Log.d("SP_SHARE", "handleIntent action: ${intent.action} type: ${intent.type}")
|
||||
|
||||
// Handle tracking notification actions
|
||||
when (intent.action) {
|
||||
TrackingForegroundService.ACTION_PAUSE -> {
|
||||
Log.d("SP_TRACKING", "Pause action received from notification")
|
||||
callJSInterfaceFunctionIfExists("next", "onPauseTracking$")
|
||||
return
|
||||
}
|
||||
TrackingForegroundService.ACTION_DONE -> {
|
||||
Log.d("SP_TRACKING", "Done action received from notification")
|
||||
callJSInterfaceFunctionIfExists("next", "onMarkTaskDone$")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle share intent
|
||||
if (Intent.ACTION_SEND == intent.action && intent.type != null) {
|
||||
if (intent.type?.startsWith("text/") == true) {
|
||||
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
package com.superproductivity.superproductivity.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
||||
class TrackingForegroundService : Service() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "TrackingService"
|
||||
|
||||
const val ACTION_START = "com.superproductivity.ACTION_START_TRACKING"
|
||||
const val ACTION_STOP = "com.superproductivity.ACTION_STOP_TRACKING"
|
||||
const val ACTION_PAUSE = "com.superproductivity.ACTION_PAUSE_TRACKING"
|
||||
const val ACTION_DONE = "com.superproductivity.ACTION_MARK_DONE"
|
||||
const val ACTION_GET_ELAPSED = "com.superproductivity.ACTION_GET_ELAPSED"
|
||||
|
||||
const val EXTRA_TASK_ID = "task_id"
|
||||
const val EXTRA_TASK_TITLE = "task_title"
|
||||
const val EXTRA_TIME_SPENT = "time_spent_ms"
|
||||
|
||||
// Static state accessible from JavaScriptInterface
|
||||
@Volatile
|
||||
var currentTaskId: String? = null
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
var startTimestamp: Long = 0
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
var accumulatedMs: Long = 0
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
var isTracking: Boolean = false
|
||||
private set
|
||||
|
||||
fun getElapsedMs(): Long {
|
||||
return if (isTracking && startTimestamp > 0) {
|
||||
(System.currentTimeMillis() - startTimestamp) + accumulatedMs
|
||||
} else {
|
||||
accumulatedMs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var taskTitle: String = ""
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val updateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isTracking) {
|
||||
updateNotification()
|
||||
handler.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Service created")
|
||||
TrackingNotificationHelper.createChannel(this)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "onStartCommand: action=${intent?.action}")
|
||||
|
||||
when (intent?.action) {
|
||||
ACTION_START -> {
|
||||
val taskId = intent.getStringExtra(EXTRA_TASK_ID) ?: return START_NOT_STICKY
|
||||
val title = intent.getStringExtra(EXTRA_TASK_TITLE) ?: "Task"
|
||||
val timeSpentMs = intent.getLongExtra(EXTRA_TIME_SPENT, 0L)
|
||||
|
||||
startTracking(taskId, title, timeSpentMs)
|
||||
}
|
||||
|
||||
ACTION_STOP -> {
|
||||
stopTracking()
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Service restarted by system - we have no state to restore
|
||||
Log.d(TAG, "Service started without action, stopping")
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startTracking(taskId: String, title: String, timeSpentMs: Long) {
|
||||
Log.d(TAG, "Starting tracking: taskId=$taskId, title=$title, timeSpentMs=$timeSpentMs")
|
||||
|
||||
currentTaskId = taskId
|
||||
taskTitle = title
|
||||
accumulatedMs = timeSpentMs
|
||||
startTimestamp = System.currentTimeMillis()
|
||||
isTracking = true
|
||||
|
||||
// Start foreground immediately to avoid ANR
|
||||
val notification = TrackingNotificationHelper.buildNotification(
|
||||
this,
|
||||
taskTitle,
|
||||
getElapsedMs()
|
||||
)
|
||||
startForeground(TrackingNotificationHelper.NOTIFICATION_ID, notification)
|
||||
|
||||
// Start update loop
|
||||
handler.removeCallbacks(updateRunnable)
|
||||
handler.post(updateRunnable)
|
||||
}
|
||||
|
||||
private fun stopTracking() {
|
||||
Log.d(TAG, "Stopping tracking, elapsed=${getElapsedMs()}ms")
|
||||
|
||||
isTracking = false
|
||||
handler.removeCallbacks(updateRunnable)
|
||||
|
||||
// Reset state
|
||||
currentTaskId = null
|
||||
startTimestamp = 0
|
||||
accumulatedMs = 0
|
||||
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
if (!isTracking) return
|
||||
|
||||
try {
|
||||
val notification = TrackingNotificationHelper.buildNotification(
|
||||
this,
|
||||
taskTitle,
|
||||
getElapsedMs()
|
||||
)
|
||||
NotificationManagerCompat.from(this).notify(
|
||||
TrackingNotificationHelper.NOTIFICATION_ID,
|
||||
notification
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "No permission to post notification", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "Service destroyed")
|
||||
isTracking = false
|
||||
handler.removeCallbacks(updateRunnable)
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
Log.d(TAG, "Task removed, stopping service")
|
||||
stopTracking()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.superproductivity.superproductivity.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.superproductivity.superproductivity.CapacitorMainActivity
|
||||
import com.superproductivity.superproductivity.R
|
||||
|
||||
object TrackingNotificationHelper {
|
||||
const val CHANNEL_ID = "sp_time_tracking_channel"
|
||||
const val NOTIFICATION_ID = 1001
|
||||
|
||||
fun createChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Time Tracking",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Shows active time tracking status"
|
||||
setShowBadge(false)
|
||||
}
|
||||
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildNotification(
|
||||
context: Context,
|
||||
taskTitle: String,
|
||||
elapsedMs: Long
|
||||
): Notification {
|
||||
val contentIntent = Intent(context, CapacitorMainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val contentPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
contentIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val pauseIntent = Intent(context, CapacitorMainActivity::class.java).apply {
|
||||
action = TrackingForegroundService.ACTION_PAUSE
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val pausePendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
1,
|
||||
pauseIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val doneIntent = Intent(context, CapacitorMainActivity::class.java).apply {
|
||||
action = TrackingForegroundService.ACTION_DONE
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val donePendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
2,
|
||||
doneIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_sp)
|
||||
.setContentTitle(taskTitle)
|
||||
.setContentText(formatDuration(elapsedMs))
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSilent(true)
|
||||
.addAction(0, "Pause", pausePendingIntent)
|
||||
.addAction(0, "Done", donePendingIntent)
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun formatDuration(ms: Long): String {
|
||||
val totalSeconds = ms / 1000
|
||||
val hours = totalSeconds / 3600
|
||||
val minutes = (totalSeconds % 3600) / 60
|
||||
val seconds = totalSeconds % 60
|
||||
|
||||
return when {
|
||||
hours > 0 -> String.format("%dh %dm %ds", hours, minutes, seconds)
|
||||
minutes > 0 -> String.format("%dm %ds", minutes, seconds)
|
||||
else -> String.format("%ds", seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.superproductivity.superproductivity.App
|
||||
import com.superproductivity.superproductivity.BuildConfig
|
||||
import com.superproductivity.superproductivity.FullscreenActivity.Companion.WINDOW_INTERFACE_PROPERTY
|
||||
import com.superproductivity.superproductivity.app.LaunchDecider
|
||||
import com.superproductivity.superproductivity.service.TrackingForegroundService
|
||||
|
||||
|
||||
class JavaScriptInterface(
|
||||
|
|
@ -71,6 +74,40 @@ class JavaScriptInterface(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun startTrackingService(taskId: String, taskTitle: String, timeSpentMs: Long) {
|
||||
val intent = Intent(activity, TrackingForegroundService::class.java).apply {
|
||||
action = TrackingForegroundService.ACTION_START
|
||||
putExtra(TrackingForegroundService.EXTRA_TASK_ID, taskId)
|
||||
putExtra(TrackingForegroundService.EXTRA_TASK_TITLE, taskTitle)
|
||||
putExtra(TrackingForegroundService.EXTRA_TIME_SPENT, timeSpentMs)
|
||||
}
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun stopTrackingService() {
|
||||
val intent = Intent(activity, TrackingForegroundService::class.java).apply {
|
||||
action = TrackingForegroundService.ACTION_STOP
|
||||
}
|
||||
activity.startService(intent)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun getTrackingElapsed(): String {
|
||||
val taskId = TrackingForegroundService.currentTaskId
|
||||
val elapsedMs = TrackingForegroundService.getElapsedMs()
|
||||
val isTracking = TrackingForegroundService.isTracking
|
||||
return if (isTracking && taskId != null) {
|
||||
"""{"taskId":"$taskId","elapsedMs":$elapsedMs}"""
|
||||
} else {
|
||||
"null"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun callJavaScriptFunction(script: String) {
|
||||
webView.post { webView.evaluateJavascript(script) { } }
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ export interface AndroidInterface {
|
|||
|
||||
triggerGetShareData?(): void;
|
||||
|
||||
// Foreground service methods for background time tracking
|
||||
startTrackingService?(taskId: string, taskTitle: string, timeSpentMs: number): void;
|
||||
stopTrackingService?(): void;
|
||||
getTrackingElapsed?(): string;
|
||||
|
||||
// added here only
|
||||
onResume$: Subject<void>;
|
||||
onPause$: Subject<void>;
|
||||
|
|
@ -50,9 +55,9 @@ export interface AndroidInterface {
|
|||
path: string;
|
||||
}>;
|
||||
|
||||
// onPauseCurrentTask$: Subject<void>;
|
||||
// onMarkCurrentTaskAsDone$: Subject<void>;
|
||||
// onAddNewTask$: Subject<void>;
|
||||
// Notification action callbacks
|
||||
onPauseTracking$: Subject<void>;
|
||||
onMarkTaskDone$: Subject<void>;
|
||||
}
|
||||
|
||||
// setInterval(() => {
|
||||
|
|
@ -68,9 +73,8 @@ if (IS_ANDROID_WEB_VIEW) {
|
|||
|
||||
androidInterface.onResume$ = new Subject();
|
||||
androidInterface.onPause$ = new Subject();
|
||||
// androidInterface.onPauseCurrentTask$ = new Subject();
|
||||
// androidInterface.onMarkCurrentTaskAsDone$ = new Subject();
|
||||
// androidInterface.onAddNewTask$ = new Subject();
|
||||
androidInterface.onPauseTracking$ = new Subject();
|
||||
androidInterface.onMarkTaskDone$ = new Subject();
|
||||
androidInterface.onShareWithAttachment$ = new ReplaySubject(1);
|
||||
androidInterface.isKeyboardShown$ = new BehaviorSubject(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { createEffect } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
pairwise,
|
||||
startWith,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view';
|
||||
import { androidInterface } from '../android-interface';
|
||||
import { TaskService } from '../../tasks/task.service';
|
||||
import { selectCurrentTask } from '../../tasks/store/task.selectors';
|
||||
import { DroidLog } from '../../../core/log';
|
||||
import { DateService } from '../../../core/date/date.service';
|
||||
import { Task } from '../../tasks/task.model';
|
||||
|
||||
@Injectable()
|
||||
export class AndroidForegroundTrackingEffects {
|
||||
private _store = inject(Store);
|
||||
private _taskService = inject(TaskService);
|
||||
private _dateService = inject(DateService);
|
||||
|
||||
/**
|
||||
* Start/stop the native foreground service when the current task changes.
|
||||
* Also handles syncing time when switching tasks directly.
|
||||
*/
|
||||
syncTrackingToService$ =
|
||||
IS_ANDROID_WEB_VIEW &&
|
||||
createEffect(
|
||||
() =>
|
||||
this._store.select(selectCurrentTask).pipe(
|
||||
distinctUntilChanged((a, b) => a?.id === b?.id),
|
||||
startWith(null as Task | null),
|
||||
pairwise(),
|
||||
tap(([prevTask, currentTask]) => {
|
||||
// If switching from one task to another (or stopping), sync the previous task's time first
|
||||
if (prevTask) {
|
||||
this._syncElapsedTimeForTask(prevTask.id);
|
||||
}
|
||||
|
||||
if (currentTask) {
|
||||
DroidLog.log('Starting tracking service', {
|
||||
taskId: currentTask.id,
|
||||
title: currentTask.title,
|
||||
timeSpent: currentTask.timeSpent,
|
||||
});
|
||||
androidInterface.startTrackingService?.(
|
||||
currentTask.id,
|
||||
currentTask.title,
|
||||
currentTask.timeSpent || 0,
|
||||
);
|
||||
} else {
|
||||
DroidLog.log('Stopping tracking service');
|
||||
androidInterface.stopTrackingService?.();
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* When the app resumes from background, sync the elapsed time from the native service.
|
||||
*/
|
||||
syncOnResume$ =
|
||||
IS_ANDROID_WEB_VIEW &&
|
||||
createEffect(
|
||||
() =>
|
||||
androidInterface.onResume$.pipe(
|
||||
withLatestFrom(this._store.select(selectCurrentTask)),
|
||||
filter(([, currentTask]) => !!currentTask),
|
||||
tap(([, currentTask]) => {
|
||||
this._syncElapsedTimeForTask(currentTask!.id);
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle pause action from the notification.
|
||||
*/
|
||||
handlePauseAction$ =
|
||||
IS_ANDROID_WEB_VIEW &&
|
||||
createEffect(
|
||||
() =>
|
||||
androidInterface.onPauseTracking$.pipe(
|
||||
withLatestFrom(this._store.select(selectCurrentTask)),
|
||||
filter(([, currentTask]) => !!currentTask),
|
||||
tap(([, currentTask]) => {
|
||||
DroidLog.log('Pause action from notification');
|
||||
// Sync elapsed time first, then pause
|
||||
this._syncElapsedTimeForTask(currentTask!.id);
|
||||
this._taskService.pauseCurrent();
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle done action from the notification.
|
||||
*/
|
||||
handleDoneAction$ =
|
||||
IS_ANDROID_WEB_VIEW &&
|
||||
createEffect(
|
||||
() =>
|
||||
androidInterface.onMarkTaskDone$.pipe(
|
||||
withLatestFrom(this._store.select(selectCurrentTask)),
|
||||
filter(([, currentTask]) => !!currentTask),
|
||||
tap(([, currentTask]) => {
|
||||
DroidLog.log('Done action from notification', { taskId: currentTask!.id });
|
||||
// Sync elapsed time, mark as done, then pause
|
||||
this._syncElapsedTimeForTask(currentTask!.id);
|
||||
this._taskService.setDone(currentTask!.id);
|
||||
this._taskService.pauseCurrent();
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
/**
|
||||
* Sync elapsed time from native service to the task.
|
||||
* Only syncs if the native service is tracking the specified task.
|
||||
*/
|
||||
private _syncElapsedTimeForTask(taskId: string): void {
|
||||
const elapsedJson = androidInterface.getTrackingElapsed?.();
|
||||
DroidLog.log('Syncing elapsed time for task', { taskId, elapsedJson });
|
||||
|
||||
if (!elapsedJson || elapsedJson === 'null') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const nativeData = JSON.parse(elapsedJson) as {
|
||||
taskId: string;
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
// Only sync if native is tracking the same task
|
||||
if (nativeData.taskId !== taskId) {
|
||||
DroidLog.log('Native tracking different task, skipping sync', {
|
||||
nativeTaskId: nativeData.taskId,
|
||||
expectedTaskId: taskId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the task to find its current timeSpent
|
||||
this._taskService
|
||||
.getByIdOnce$(taskId)
|
||||
.subscribe((task) => {
|
||||
if (!task) {
|
||||
DroidLog.log('Task not found for sync', { taskId });
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTimeSpent = task.timeSpent || 0;
|
||||
const duration = nativeData.elapsedMs - currentTimeSpent;
|
||||
|
||||
DroidLog.log('Calculated sync duration', {
|
||||
taskId,
|
||||
nativeElapsed: nativeData.elapsedMs,
|
||||
currentTimeSpent,
|
||||
duration,
|
||||
});
|
||||
|
||||
if (duration > 0) {
|
||||
this._taskService.addTimeSpent(task, duration, this._dateService.todayStr());
|
||||
}
|
||||
})
|
||||
.unsubscribe();
|
||||
} catch (e) {
|
||||
DroidLog.err('Failed to parse elapsed time', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ import { workContextReducer } from '../features/work-context/store/work-context.
|
|||
import { WorkContextEffects } from '../features/work-context/store/work-context.effects';
|
||||
import { IS_ANDROID_WEB_VIEW } from '../util/is-android-web-view';
|
||||
import { AndroidEffects } from '../features/android/store/android.effects';
|
||||
import { AndroidForegroundTrackingEffects } from '../features/android/store/android-foreground-tracking.effects';
|
||||
import { CaldavIssueEffects } from '../features/issue/providers/caldav/caldav-issue.effects';
|
||||
import { CalendarIntegrationEffects } from '../features/calendar-integration/store/calendar-integration.effects';
|
||||
import { ElectronEffects } from '../core/electron/electron.effects';
|
||||
|
|
@ -151,7 +152,9 @@ import { PluginHooksEffects } from '../plugins/plugin-hooks.effects';
|
|||
EffectsModule.forFeature([PlannerEffects]),
|
||||
|
||||
// EFFECTS ONLY
|
||||
EffectsModule.forFeature([...(IS_ANDROID_WEB_VIEW ? [AndroidEffects] : [])]),
|
||||
EffectsModule.forFeature([
|
||||
...(IS_ANDROID_WEB_VIEW ? [AndroidEffects, AndroidForegroundTrackingEffects] : []),
|
||||
]),
|
||||
EffectsModule.forFeature([CaldavIssueEffects]),
|
||||
EffectsModule.forFeature([CalendarIntegrationEffects]),
|
||||
EffectsModule.forFeature([ElectronEffects]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue