Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ open class MessageLocation(
BEACON("b"), // Generated by iOS beacons
IOS_FREQUENT_LOCATIONS("v"), // Generated by iOS frequent locations
IOS_FOLLOW_CIRCULAR("C"), // Generated by iOS follow circular region
SIGNIFICANT_MOTION("m"), // Triggered by significant motion sensor
DEFAULT("")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -429,11 +429,14 @@ constructor(
const val EXPERIMENTAL_FEATURE_SHOW_EXPERIMENTAL_PREFERENCE_UI = "showExperimentalPreferenceUI"
const val EXPERIMENTAL_FEATURE_LOCATION_PING_USES_HIGH_ACCURACY_LOCATION_REQUEST =
"locationPingUsesHighAccuracyLocationRequest"
const val EXPERIMENTAL_FEATURE_REQUEST_LOCATION_ON_SIGNIFICANT_MOTION =
"requestLocationOnSignificantMotion"

internal val EXPERIMENTAL_FEATURES =
setOf(
EXPERIMENTAL_FEATURE_SHOW_EXPERIMENTAL_PREFERENCE_UI,
EXPERIMENTAL_FEATURE_LOCATION_PING_USES_HIGH_ACCURACY_LOCATION_REQUEST)
EXPERIMENTAL_FEATURE_LOCATION_PING_USES_HIGH_ACCURACY_LOCATION_REQUEST,
EXPERIMENTAL_FEATURE_REQUEST_LOCATION_ON_SIGNIFICANT_MOTION)

val SYSTEM_NIGHT_AUTO_MODE by lazy {
if (SDK_INT > Build.VERSION_CODES.Q) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ class BackgroundService : LifecycleService(), Preferences.OnPreferenceChangeList
}
}

// Significant motion sensor for triggering location requests when device movement is detected
private lateinit var significantMotionSensor: SignificantMotionSensor

@EntryPoint
@InstallIn(SingletonComponent::class)
internal interface ServiceEntrypoint {
Expand Down Expand Up @@ -172,6 +175,16 @@ class BackgroundService : LifecycleService(), Preferences.OnPreferenceChangeList

preferences.registerOnPreferenceChangedListener(this)

// Initialize significant motion sensor
significantMotionSensor =
SignificantMotionSensor(
this,
preferences,
locationProviderClient,
requirementsChecker,
callbackForReportType[MessageLocation.ReportType.SIGNIFICANT_MOTION]!!.value,
runThingsOnOtherThreads.getBackgroundLooper())

registerReceiver(
powerBroadcastReceiver,
IntentFilter().apply {
Expand Down Expand Up @@ -234,6 +247,7 @@ class BackgroundService : LifecycleService(), Preferences.OnPreferenceChangeList
Timber.v("Backgroundservice onDestroy")
stopForeground(STOP_FOREGROUND_REMOVE)
unregisterReceiver(powerBroadcastReceiver)
significantMotionSensor.cancel()
preferences.unregisterOnPreferenceChangedListener(this)
messageProcessor.stopSendingMessages()
super.onDestroy()
Expand Down Expand Up @@ -351,6 +365,7 @@ class BackgroundService : LifecycleService(), Preferences.OnPreferenceChangeList
startForegroundService()
setupLocationRequest()
scheduler.scheduleLocationPing()
significantMotionSensor.setup()
messageProcessor.initialize()
}

Expand Down Expand Up @@ -630,6 +645,17 @@ class BackgroundService : LifecycleService(), Preferences.OnPreferenceChangeList
if (properties.intersect(PREFERENCES_THAT_WIPE_QUEUE_AND_CONTACTS).isNotEmpty()) {
lifecycleScope.launch { contactsRepo.clearAll() }
}
if (properties.contains(Preferences::experimentalFeatures.name)) {
// Handle significant motion sensor based on experimental feature toggle
if (preferences.experimentalFeatures.contains(
Preferences.EXPERIMENTAL_FEATURE_REQUEST_LOCATION_ON_SIGNIFICANT_MOTION)) {
Timber.d("Significant motion feature enabled, setting up sensor")
significantMotionSensor.setup()
} else {
Timber.d("Significant motion feature disabled, cancelling sensor")
significantMotionSensor.cancel()
}
}
}

fun reInitializeLocationRequests() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Assisted-by: Claude Code IDE; model: claude-4.5-sonnet

package org.owntracks.android.services

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import android.hardware.TriggerEvent
import android.hardware.TriggerEventListener
import android.os.Looper
import android.os.SystemClock
import java.util.concurrent.TimeUnit
import org.owntracks.android.location.LocationCallback
import org.owntracks.android.location.LocationProviderClient
import org.owntracks.android.preferences.Preferences
import org.owntracks.android.preferences.types.MonitoringMode
import org.owntracks.android.support.RequirementsChecker
import timber.log.Timber

/**
* Manages the significant motion sensor for triggering location requests when device movement is
* detected.
*
* The significant motion sensor is a one-shot wake-up sensor that triggers when the device detects
* significant movement (like walking). After triggering, the listener must be re-registered.
*/
class SignificantMotionSensor(
private val context: Context,
private val preferences: Preferences,
private val locationProviderClient: LocationProviderClient,
private val requirementsChecker: RequirementsChecker,
private val locationCallback: LocationCallback,
private val looper: Looper
) {
private val sensorManager by lazy {
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}

private val significantMotionSensor: Sensor? by lazy {
sensorManager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)
}

// Track when we last requested location due to significant motion (for rate limiting)
@Volatile private var lastSignificantMotionLocationRequestTime: Long = 0L

private val significantMotionTriggerListener =
object : TriggerEventListener() {
override fun onTrigger(event: TriggerEvent?) {
Timber.d("Significant motion detected")
onSignificantMotionDetected()
}
}

/**
* Sets up the significant motion sensor listener. This sensor triggers a location request when
* significant movement is detected.
*/
fun setup() {
if (!preferences.experimentalFeatures.contains(
Preferences.EXPERIMENTAL_FEATURE_REQUEST_LOCATION_ON_SIGNIFICANT_MOTION)) {
Timber.d("Significant motion sensor disabled (experimental feature not enabled)")
return
}
significantMotionSensor?.let { sensor ->
Timber.d(
"Found significant motion sensor: ${sensor.name} (vendor: ${sensor.vendor}, isWakeUpSensor: ${sensor.isWakeUpSensor})")
// Cancel any existing registration before requesting a new one
// (requestTriggerSensor fails if listener is already registered)
sensorManager.cancelTriggerSensor(significantMotionTriggerListener, sensor)
val success = sensorManager.requestTriggerSensor(significantMotionTriggerListener, sensor)
if (success) {
Timber.d("Significant motion sensor listener registered successfully")
} else {
Timber.w("Failed to register significant motion sensor listener")
}
}
?: run {
// Log available sensors to help diagnose issues
val allSensors = sensorManager.getSensorList(Sensor.TYPE_ALL)
val motionRelatedSensors =
allSensors.filter {
it.type == Sensor.TYPE_SIGNIFICANT_MOTION ||
it.name.contains("motion", ignoreCase = true) ||
it.name.contains("movement", ignoreCase = true)
}
Timber.w(
"Significant motion sensor not available. Motion-related sensors found: ${motionRelatedSensors.map { "${it.name} (type=${it.type})" }}")
}
}

/**
* Cancels the significant motion sensor listener. Should be called when the feature is disabled
* or when cleaning up resources.
*/
fun cancel() {
significantMotionSensor?.let { sensor ->
sensorManager.cancelTriggerSensor(significantMotionTriggerListener, sensor)
Timber.d("Significant motion sensor listener cancelled")
}
}

/**
* Called when significant motion is detected. Requests a high-accuracy GPS location if enough
* time has passed since the last request (rate limited based on
* pegLocatorFastestIntervalToInterval, locatorInterval and moveModeLocatorInterval). Always
* re-registers the trigger listener (since TYPE_SIGNIFICANT_MOTION is a one-shot sensor).
*/
private fun onSignificantMotionDetected() {
// Re-register the trigger listener first (TYPE_SIGNIFICANT_MOTION is a one-shot sensor)
// We do this regardless of rate limiting so we don't miss future motion events
setup()

// Rate limit: use the same interval logic as BackgroundService location requests
val now = SystemClock.elapsedRealtime()
val intervalSeconds =
if (preferences.monitoring == MonitoringMode.Move) {
preferences.moveModeLocatorInterval
} else {
preferences.locatorInterval
}
val minIntervalMs =
if (preferences.pegLocatorFastestIntervalToInterval) {
TimeUnit.SECONDS.toMillis(intervalSeconds.toLong())
} else {
TimeUnit.SECONDS.toMillis(1)
}
val timeSinceLastRequest = now - lastSignificantMotionLocationRequestTime

if (timeSinceLastRequest < minIntervalMs) {
Timber.d(
"Significant motion detected but rate limited. " +
"Time since last request: ${timeSinceLastRequest}ms, min interval: ${minIntervalMs}ms")
return
}

if (requirementsChecker.hasLocationPermissions()) {
Timber.d("Requesting high-accuracy location due to significant motion detection")
lastSignificantMotionLocationRequestTime = now
locationProviderClient.singleHighAccuracyLocation(locationCallback, looper)
} else {
Timber.w("Missing location permission, cannot request location for significant motion")
}
}
}