Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0b6a05b
Prepare favorites work
KacperKozak Jul 27, 2026
da34ba2
Define Favorites domain
KacperKozak Jul 27, 2026
13482b8
Decouple Favorites from sync
KacperKozak Jul 28, 2026
2cc6b17
Keep Favorites scope local
KacperKozak Jul 28, 2026
5f24d4a
Add Favorites table, bridge API and Favorites tab #287
KacperKozak Jul 28, 2026
091c74d
Key favorites by board_id instead of ble device id #287
KacperKozak Jul 28, 2026
8a36f96
Guard favorite star against double taps and surface failures #287
KacperKozak Jul 28, 2026
8a5abd3
Trim range selection for Favorites #288
KacperKozak Jul 28, 2026
4eda8a4
Merge remote-tracking branch 'origin/dev' into favorites
KacperKozak Jul 29, 2026
c562d14
Merge remote-tracking branch 'origin/dev' into favorites
KacperKozak Jul 29, 2026
b0e4792
Pin favorited telemetry during deletion #289
KacperKozak Jul 29, 2026
a422bce
Avoid bucket rebuilds for favorite deletion #289
KacperKozak Jul 29, 2026
37e12de
Add favorite detail and rename #290
KacperKozak Jul 29, 2026
3fae055
Store Favorite Media natively #291
KacperKozak Jul 30, 2026
68f544b
Fix favorite back
KacperKozak Jul 30, 2026
ccd94c1
Fix favorites navigation
KacperKozak Jul 30, 2026
8a5ddd2
Polish favorites controls
KacperKozak Jul 30, 2026
094317b
Improve favorite trim
KacperKozak Jul 30, 2026
6668bb9
Improve favorites
KacperKozak Jul 30, 2026
4dba8ea
Suggest ride names
KacperKozak Jul 30, 2026
4998f00
Fix history chart time
KacperKozak Jul 30, 2026
5771ac6
Fix history drawer
KacperKozak Jul 30, 2026
0b12793
Fix review feedback
KacperKozak Jul 30, 2026
9c91dba
Polish favorites
KacperKozak Jul 30, 2026
e964cec
Mark favorite routes
KacperKozak Jul 30, 2026
7318a2e
Mark favorite chart ranges
KacperKozak Jul 30, 2026
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
13 changes: 9 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,13 @@ _Avoid_: Trim range, active range, ride duration
A temporary state of a Ride Recording in which sample persistence halts because the Board has produced no moving Telemetry Sample for a sustained interval, while the Board Session stays live at a reduced poll rate and auto-resumes on the next moving sample. Cuts battery, stored frames, and bucket sample counts together while the board is parked.
_Avoid_: Stop recording, auto-stop, sleep, parked mode

**Media History Asset**:
A phone photo or video whose capture time falls inside a selected Ride Recording and which can be placed using a nearby recording-backed GPS fix. The asset remains owned by the OS photo library and is never copied into Ride History.
_Avoid_: Ride photo, recording media, uploaded media
**Favorite**:
A user-created, optionally named durable time range over Ride History, created by trimming a past ride to the span the rider wants to keep. A ride may produce multiple Favorites. Its name can be changed or cleared later, and re-trimming updates its range and recomputed summary while preserving its identity and Favorite Media. A Favorite pins its telemetry range: history deletion skips favorited ranges, and removing a Favorite only unpins — it never deletes telemetry. Owns its Favorite Media.
_Avoid_: Favorite ride, segment, bookmark, saved ride

**Favorite Media**:
A photo or video the rider explicitly attached to a Favorite, copied from the OS picker into app storage owned by that Favorite and recorded in the native Favorite Media manifest. Placed on the map using a nearby recording-backed GPS fix by capture time. Deleted together with its Favorite.
_Avoid_: Media History Asset, ride photo, gallery match, uploaded media

**Map Point**:
A globally shared, Account-authored map-visible location that is independent from Ride Recording and Ride History. A Map Point describes a categorized riding place such as a drop, bonk, trail entry, viewpoint, or charging place; a personal navigation target is not a Map Point. Reading a Map Point needs no account; contributing or changing one requires sign-in.
Expand Down Expand Up @@ -321,7 +325,8 @@ _Avoid_: Position update, presence ping, location share, group telemetry
- A **Moving Window** belongs to one **Ride Recording** and is derived from which **Telemetry Samples** are excluded from speed metrics; a Ride Recording without one is excluded from **Ride History**.
- A **Ride History Marker** belongs to **Ride History** and may explain where a **Ride Recording** lost or regained board data.
- An **Idle Pause** belongs to one **Ride Recording**, begins after a sustained absence of moving **Telemetry Samples**, keeps the **Board Session** live at a reduced poll rate, and produces a **Ride History Marker**; its sample gap stays inside the **Moving Window** (and counts toward ride time) when it occurs between two moving spans.
- A **Media History Asset** is a local-only view of an OS photo-library asset matched to one selected **Ride Recording** by capture time and placed from a nearby recording-backed **GPS Fix**.
- A **Favorite** is a durable time range over **Ride History**; its telemetry is pinned against deletion, and a deleted ride leaves its favorited sub-ranges intact.
- **Favorite Media** belongs to one **Favorite**, is copied into app storage, and is placed from a nearby recording-backed **GPS Fix** by capture time.
- A **Tune Snapshot** belongs to the currently connected **Board** and is read-only.
- A **Tune Profile** belongs to a **Board** and stores semantic field values independently of firmware schema.
- A **Tune Profile** also belongs to one **Tune Compatibility**; profiles from other Refloat package versions are retained but not used for the current board state.
Expand Down
2 changes: 2 additions & 0 deletions docs/adr/0014-media-history-is-a-local-derived-view.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Media History is a local derived view

> **Superseded by ADR 0030.** Google Play policy blocked broad gallery reads; media moved to an explicit picker flow with copied, Favorite-owned storage.

Media History displays phone photos and videos alongside a selected Ride Recording without making those assets part of durable Ride History. The OS photo library remains durable media truth; native Ride Recording storage remains durable ride truth. Matching is recomputed when Media History is read for a selected ride and is not persisted.

## Matching Contract
Expand Down
26 changes: 26 additions & 0 deletions docs/adr/0029-favorites-pin-telemetry-ranges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Favorites pin telemetry ranges

A Favorite is a durable, optionally named time range `[startMs, endMs]` over telemetry history, stored in a native table in the telemetry DB on both platforms. It is not a pointer to a ride: history sessions are derived on read (ADR 0004/0005) and have no stable identity, while a time range survives regrouping and allows multiple Favorites per ride, including trimmed sub-ranges selected on the ride timeline.

## Contract

- Favorites live in a native table (`@parity` iOS/Android) so telemetry deletion paths can see them.
- A Favorite has a native-minted stable UUID plus native-owned `created_at` and `updated_at`; JS cannot supply them.
- Re-trimming or renaming updates the existing Favorite row in place. Its UUID, `created_at`, Board ownership, and Favorite Media remain stable; native mints a new `updated_at`.
- `deleteTelemetryRange` and `clearTelemetryHistory` protect every minute bucket touched by a favorited range. Both the precomputed bucket and all its raw samples stay together; only buckets and telemetry wholly outside those bucket-aligned protected ranges are deleted. Deleting a ride around a Favorite leaves the protected buckets as a short standalone ride.
- Rides containing a favorited range are marked in history as not fully deletable.
- Removing a Favorite only unpins: its telemetry stays and becomes deletable like any ride. Its Favorite Media is deleted with it.
- Summary stats (mirroring history session summary fields) are computed from raw samples whenever the range is created or updated and denormalized onto the row (ADR 0005 style); the route preview is derived on read from pinned samples.

## Considered Options

- **Favorite references a session id.** Rejected: session ids are synthesized by grouping and unstable.
- **Cascade delete favorites with their ride.** Rejected: starring means "keep this"; deletion silently destroying favorites betrays that intent.
- **JS-side favorite store passing protected ranges into native deletes.** Rejected: native truth would depend on JS remembering to send it.
- **Delete pinned telemetry when its Favorite is removed.** Rejected: unfavoriting silently destroying telemetry is surprising; unpin-only keeps one rule.

## Consequences

- Delete paths subtract bucket-aligned protected ranges. This deliberately keeps up to 59 seconds of telemetry beyond each exact Favorite edge so bucket summaries stay truthful without rebuilding the full telemetry database; the Favorite's own range and denormalized summary remain exact.
- Favorited telemetry is exempt from any future retention pruning.
- Orphan favorite islands appear in History after surrounding-ride deletion; this is accepted as honest.
25 changes: 25 additions & 0 deletions docs/adr/0030-favorite-media-is-curated-copied-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Favorite Media is curated, copied, favorite-owned storage

Supersedes ADR 0014 (Media History is a local derived view). The derived-on-read photo-library model was already abandoned in practice — Google Play policy blocked broad gallery reads, so media moved to an explicit picker flow that copies files into app storage (`rideMediaFiles.ts`). This ADR makes the current model official and re-keys it to Favorites.

## Contract

- Media attaches only to Favorites. The rider explicitly picks assets; there is no automatic photo-library matching.
- Native owns a `favorite_media` manifest on both platforms. Each immutable row has a native-minted stable UUID primary key, `favorite_id`, capture time, MIME/media kind, byte count, SHA-256 content hash, and creation time.
- Picked files are imported into a canonical per-Favorite/per-media path in app storage. The manifest is durable metadata truth; filenames do not encode metadata.
- Map placement uses the nearest recording-backed GPS fix to capture time, as before. Asset GPS metadata is ignored.
- Deleting a Favorite raw-deletes its manifest rows as a parent-covered cascade and best-effort deletes its media directory. Reconciliation removes incomplete imports and orphaned files.
- Legacy `rideMedia/<sessionId>` folders are left untouched; no migration (PoC).

## Considered Options

- **Keep session-id keying and gate UI to favorites.** Rejected: session ids are derived and unstable; the Favorite is the durable owner.
- **Keep the filesystem as the only record.** Rejected: metadata encoded only in filenames is fragile to enumerate, validate, and reconcile after interrupted operations.
- **Derived photo-library matching scoped to favorites.** Rejected: Play policy already forced the picker model; broad gallery reads are not coming back.
- **Migrate existing per-ride media into intersecting favorites.** Rejected as not worth it for a PoC.

## Consequences

- The native manifest owns durable Favorite Media metadata; app storage owns the local bytes. Assets survive photo-library changes but cost disk space.
- Import and deletion cross SQLite and the filesystem, so reconciliation is required to repair interrupted operations.
- Photos on non-favorited rides are no longer possible; favoriting is the gateway to attaching media.
15 changes: 15 additions & 0 deletions e2e/flows/edge-drawer-focus.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
appId: ${APP_ID}
---
- runFlow: _launch.yaml
- openLink: 'vescape://settings/components/modals'
- scrollUntilVisible:
element:
id: edge-drawer-focus-open
direction: DOWN
- tapOn:
id: edge-drawer-focus-open
- extendedWaitUntil:
visible: Focused list expanded
timeout: 5000
- assertVisible:
id: edge-drawer-focused-row
53 changes: 50 additions & 3 deletions e2e/flows/history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ appId: ${APP_ID}
# Newest ride stats (selected automatically).
- assertVisible: Distance
- assertVisible: Top Speed
- assertVisible: '1.5 km'
- assertVisible: '1.5'
- assertVisible: '25 km/h'

# Expand the stats panel to verify secondary stats are computed too.
Expand All @@ -42,7 +42,7 @@ appId: ${APP_ID}
- tapOn:
id: history-previous-ride
- extendedWaitUntil:
visible: '800 m'
visible: '800'
timeout: 5000
- assertVisible: '20 km/h'

Expand All @@ -54,7 +54,7 @@ appId: ${APP_ID}
- tapOn:
id: history-next-ride
- extendedWaitUntil:
visible: '1.5 km'
visible: '1.5'
timeout: 5000
- assertVisible: '25 km/h'

Expand All @@ -69,3 +69,50 @@ appId: ${APP_ID}
# Close the sheet by tapping the backdrop.
- tapOn:
id: history-session-sheet-backdrop

# Favorites is a filtered History tab: newest Favorite opens immediately and keeps the same nav.
- tapOn:
id: history-favorite-ride
- assertVisible:
id: trim-favorite-name
- assertNotVisible:
id: history-ride-list-button
- tapOn:
id: trim-favorite-name
- eraseText
- inputText: Evening ride
- tapOn:
id: trim-save
# Saving opens the new Favorite immediately.
- extendedWaitUntil:
visible:
id: favorite-edit
timeout: 5000
- assertVisible: Evening ride
- tapOn:
id: favorite-edit
- assertVisible:
id: trim-favorite-name
- tapOn:
id: trim-favorite-name
- eraseText
- inputText: Edited ride
- tapOn:
id: trim-save
- extendedWaitUntil:
visible:
id: favorite-edit
timeout: 5000
- assertVisible: Edited ride
- tapOn:
id: history-ride-list-button
- extendedWaitUntil:
visible:
id: history-session-sheet
timeout: 5000
- tapOn:
id: history-session-sheet-backdrop
- tapOn:
id: history-tab-history
- assertNotVisible: No rides yet
- assertVisible: '1.5'
4 changes: 4 additions & 0 deletions modules/vescape-core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ android {
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 26)
targetSdkVersion safeExtGet("targetSdkVersion", 35)
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
Expand Down Expand Up @@ -59,4 +60,7 @@ dependencies {
testImplementation "junit:junit:4.13.2"
testImplementation "org.json:json:20231013"
testImplementation "androidx.room:room-testing:2.8.4"
androidTestImplementation "androidx.test:core:1.6.1"
androidTestImplementation "androidx.test.ext:junit:1.2.1"
androidTestImplementation "androidx.test:runner:1.6.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package expo.modules.vescapecore.telemetry

import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class FavoriteDaoTest {
private lateinit var database: TelemetryDatabase
private lateinit var dao: TelemetryDao

@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, TelemetryDatabase::class.java)
.allowMainThreadQueries()
.build()
dao = database.telemetryDao()
}

@After
fun tearDown() {
database.close()
}

@Test
fun favoriteCrudRoundTrip() = runBlocking {
val older = favorite(id = "older", name = null, startMs = 1_000, updatedAt = 1_000)
val newer = favorite(id = "newer", name = "Evening ride", startMs = 3_000, updatedAt = 3_000)

dao.insertFavorite(older)
dao.insertFavorite(newer)
assertEquals(listOf(newer, older), dao.getFavorites())

val renamed = newer.copy(name = "Night ride", endMs = 4_500, updatedAt = 4_500)
assertEquals(1, dao.updateFavorite(renamed))
assertEquals(renamed, dao.getFavorite(newer.id))

assertEquals(1, dao.deleteFavorite(newer.id))
assertNull(dao.getFavorite(newer.id))
assertEquals(listOf(older), dao.getFavorites())
}

private fun favorite(
id: String,
name: String?,
startMs: Long,
updatedAt: Long,
) = FavoriteEntity(
id = id,
boardId = null,
name = name,
startMs = startMs,
endMs = startMs + 1_000,
createdAt = startMs,
updatedAt = updatedAt,
sampleCount = 10,
gpsPointCount = 5,
distanceCm = 120_000,
movingDurationMs = 60_000,
avgSpeedCentiKmh = 2_000,
maxSpeedCentiKmh = 3_000,
batteryUsedWhMilli = 1_500,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
Expand Down Expand Up @@ -545,6 +546,29 @@ class VescapeCoreModule : Module() {
AsyncFunction("getProfileStatMonths") {
runBlocking { ProfileStatsRepository.get(context.applicationContext).getProfileStatMonths() }
}
// Favorites (ADR 0029). JS supplies only the range and an optional name; identity, timestamps
// and the denormalized summary are native.
// @parity /modules/vescape-core/ios/VescapeCoreModule.swift `getFavorites`
AsyncFunction("getFavorites") Coroutine { ->
TelemetryRepository.get(context.applicationContext).getFavorites()
}
AsyncFunction("createFavorite") Coroutine { options: Map<String, Any?> ->
TelemetryRepository.get(context.applicationContext).createFavorite(options)
?: throw CodedException("ERR_CREATE_FAVORITE", "favorite range is invalid or could not be stored", null)
}
AsyncFunction("updateFavorite") Coroutine { id: String, options: Map<String, Any?> ->
TelemetryRepository.get(context.applicationContext).updateFavorite(id, options)
?: throw CodedException("ERR_UPDATE_FAVORITE", "favorite does not exist or could not be stored", null)
}
Comment on lines +559 to +562

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'modules/vescape-core/(android|ios)/|docs/board-warnings.md|TelemetryRepository' || true

echo "== kotlin updateFavorite sites =="
rg -n "fun updateFavorite|updateFavorite|require\(endMs >= startMs\)|ERR_UPDATE_FAVORITE|updateFavorite" modules/vescape-core -S

echo "== target file excerpt =="
sed -n '530,575p' modules/vescape-core/android/src/main/java/expo/modules/vescapecore/VescapeCoreModule.kt 2>/dev/null || true

echo "== android telemetry repo excerpt if exists =="
f=$(git ls-files | rg 'TelemetryRepository\.kt$|VescapeCoreModule\.kt$')
for file in $f; do
  case "$file" in
    *TelemetryRepository.kt)
      echo "--- $file"
      wc -l "$file"
      rg -n "updateFavorite|require|endMs|startMs" "$file" -A8 -B8
      ;;
  esac
done

Repository: KacperKozak/vescape

Length of output: 41092


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== iOS updateFavorite implementation =="
sed -n '300,360p' modules/vescape-core/ios/telemetry/TelemetryRepository.swift
sed -n '628,648p' modules/vescape-core/ios/VescapeCoreModule.swift

echo "== Android code that maps Java exceptions to JS errors =="
f=$(git ls-files | rg 'modules/vescape-core/android/src/main/java/expo/modules/vescapecore/VescapeCoreModule.kt$|ErrorReporting|Exception')
rg -n "CodedException|IllegalArgumentException|java\\.lang\\.IllegalArgumentException|onCodedException|ErrorReporting|MapString" modules/vescape-core/android/src/main/java/expo/modules/vescapecore -S

echo "== Kotlin optional conversion for map? =="
sed -n '550,565p' modules/vescape-core/android/src/main/java/expo/modules/vescapecore/VescapeCoreModule.kt

echo "== read-only semantics probe for Kotlin-like Kotlin require ==
python3 - <<'PY'
def update_favorite(id_, existing, options):
    if existing is None:
        return None
    start_ms = options["startMs"]
    end_ms = options["endMs"]
    if not (end_ms >= start_ms):
        raise ValueError("endMs must be greater than or equal to startMs")
    if options.get("exists") is False:
        return None
    return {"id": id_}

for name, args in [
    ("valid", ("/", {"exists": True, "startMs": 10, "endMs": 20})),
    ("invalid_range", ("/", {"exists": True, "startMs": 20, "endMs": 10})),
    ("not_exist", ("/", {"exists": False, "startMs": 10, "endMs": 20})),
]:
    try:
        result = update_favorite(*args)
        print(f"{name}: result={result}, null_guard_taken={result is None}")
    except Exception as e:
        print(f"{name}: throws={type(e).__name__}: {e}")
PY

Repository: KacperKozak/vescape

Length of output: 9709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Java exception mapping for this module =="
sed -n '890,915p' modules/vescape-core/android/src/main/java/expo/modules/vescapecore/VescapeCoreModule.kt

echo "== Android updateFavorite parity call context =="
sed -n '555,563p' modules/vescape-core/android/src/main/java/expo/modules/vescapecore/VescapeCoreModule.kt

echo "== Android parseLong behavior probe in sandbox =="
java -Xmx64m -version >/dev/null 2>&1 || true
if command -v java >/dev/null 2>&1; then
  cat > /tmp/RequireProbe.java <<'JAVA'
public class RequireProbe {
  public static void main(String[] args) {
    for (String[] pair : new String[][]{{"10","20"},{"20","10"}}) {
      long startMs = Long.parseLong(pair[0]);
      long endMs = Long.parseLong(pair[1]);
      try {
        assertTrue(endMs >= startMs, "endMs must be greater than or equal to startMs");
        System.out.println(pair[0]+" -> "+pair[1]+" result=return value");
      } catch (IllegalArgumentException e) {
        System.out.println(pair[0]+" -> "+pair[1]+" throws=IllegalArgumentException: "+e.getCause());
      }
    }
  }
  static void assertTrue(boolean condition, String msg) { if (!condition) throw new IllegalArgumentException(msg); }
}
JAVA
  javac /tmp/RequireProbe.java
  java -cp /tmp RequireProbe
fi

Repository: KacperKozak/vescape

Length of output: 1817


Handle invalid favorite ranges as ERR_UPDATE_FAVORITE before the null guard.

TelemetryRepository.updateFavorite already calls require(endMs >= start_ms) before returning a null favorite, so endMs < startMs throws IllegalArgumentException instead of returning null. Align Android with iOS by either validating the range and returning null, or catching and re-throwing it as the controlled ERR_UPDATE_FAVORITE rejection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/vescape-core/android/src/main/java/expo/modules/vescapecore/VescapeCoreModule.kt`
around lines 558 - 561, Update the updateFavorite AsyncFunction to handle
invalid ranges where endMs is less than startMs before the existing null guard,
ensuring the operation rejects with CodedException code ERR_UPDATE_FAVORITE
rather than leaking IllegalArgumentException. Either validate and return the
same controlled error or catch the repository exception and rethrow it as
ERR_UPDATE_FAVORITE, while preserving the existing missing-favorite behavior.

Source: Coding guidelines

AsyncFunction("deleteFavorite") Coroutine { id: String ->
TelemetryRepository.get(context.applicationContext).deleteFavorite(id)
}
AsyncFunction("getFavoriteMedia") Coroutine { favoriteId: String ->
TelemetryRepository.get(context.applicationContext).getFavoriteMedia(favoriteId)
}
AsyncFunction("importFavoriteMedia") Coroutine { options: Map<String, Any?> ->
TelemetryRepository.get(context.applicationContext).importFavoriteMedia(options)
}
AsyncFunction("deleteTelemetryBefore") Coroutine { beforeMs: Double ->
TelemetryRepository.get(context.applicationContext).deleteBefore(beforeMs.toLong())
}
Expand Down
Loading
Loading