Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vibrate when compass crosses north #47

Merged
merged 2 commits into from
Jan 7, 2024
Merged
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
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
android:name="android.hardware.sensor.compass"
android:required="false" />

<uses-permission android:name="android.permission.VIBRATE" />

<application
android:name=".PositionalApplication"
android:dataExtractionRules="@xml/backup_rules"
Expand Down
49 changes: 41 additions & 8 deletions app/src/main/kotlin/io/trewartha/positional/ui/compass/Compass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
Expand All @@ -24,14 +28,15 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.google.android.material.color.MaterialColors.harmonize
import com.google.firebase.crashlytics.FirebaseCrashlytics
import io.trewartha.positional.R
import io.trewartha.positional.data.measurement.Angle
import io.trewartha.positional.data.ui.CompassNorthVibration
import io.trewartha.positional.ui.PositionalTheme
import io.trewartha.positional.ui.locals.LocalVibrator
import io.trewartha.positional.ui.utils.placeholder
import kotlin.math.cos
import kotlin.math.roundToInt
Expand All @@ -40,13 +45,15 @@ import kotlin.math.sin
@Composable
fun Compass(
azimuth: Angle?,
northVibration: CompassNorthVibration?,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.placeholder(visible = azimuth == null),
contentAlignment = Alignment.Center
) {
if (azimuth != null) {
NorthVibration(azimuth, northVibration)
CompassReading(azimuth)
CompassRose(azimuth, Modifier.fillMaxSize())
}
Expand Down Expand Up @@ -276,7 +283,28 @@ private fun DirectionText(azimuth: Angle, modifier: Modifier = Modifier) {
}
}

@Composable
private fun NorthVibration(azimuth: Angle, northVibration: CompassNorthVibration?) {
var previousQuadrant by remember { mutableStateOf<Quadrant?>(null) }
val currentQuadrant by remember(azimuth) { derivedStateOf { azimuth.quadrant } }
val crossedNorth = previousQuadrant != null &&
((previousQuadrant == Quadrant.NW && currentQuadrant == Quadrant.NE) ||
(previousQuadrant == Quadrant.NE && currentQuadrant == Quadrant.NW))
previousQuadrant = currentQuadrant
val vibrator = LocalVibrator.current
LaunchedEffect(crossedNorth) {
if (crossedNorth && northVibration != null) {
@Suppress("DEPRECATION") // It matches our needs and goes back pre API 21
vibrator.vibrate(northVibration.duration.inWholeMilliseconds)
}
}
}

private const val AZIMUTH_DEFAULT = 0f
private const val AZIMUTH_N = 0f
private const val AZIMUTH_E = 90f
private const val AZIMUTH_S = 180f
private const val AZIMUTH_W = 270f
private const val AZIMUTH_N_MIN = 337.5f
private const val AZIMUTH_N_MAX = 22.5f
private const val AZIMUTH_E_MIN = 67.5f
Expand Down Expand Up @@ -310,21 +338,26 @@ private val TICK_MAJOR_LENGTH = 16.dp
private val TICK_MINOR_WIDTH = 8.dp
private val TICK_MINOR_LENGTH = 8.dp

private data class TickStyle(
val color: Color,
val lengthPx: Float,
val widthPx: Float
)
private enum class Quadrant { NE, SE, SW, NW }

private data class TickStyle(val color: Color, val lengthPx: Float, val widthPx: Float)

private val Angle.quadrant: Quadrant
get() = when (inDegrees().value) {
in AZIMUTH_N..AZIMUTH_E -> Quadrant.NE
in AZIMUTH_E..AZIMUTH_S -> Quadrant.SE
in AZIMUTH_S..AZIMUTH_W -> Quadrant.SW
else -> Quadrant.NW
}

private fun Float.toRadians(): Float = (this / DEGREES_180 * Math.PI).toFloat()

@PreviewLightDark
@PreviewScreenSizes
@Composable
private fun CompassPreview() {
PositionalTheme {
Surface(Modifier.size(600.dp, 300.dp)) {
Compass(azimuth = Angle.Degrees(25f))
Compass(azimuth = Angle.Degrees(25f), northVibration = CompassNorthVibration.SHORT)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.trewartha.positional.ui.compass

import android.content.Context
import android.os.Vibrator
import android.view.Surface
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand Down Expand Up @@ -29,6 +30,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -54,13 +56,15 @@ import io.trewartha.positional.data.compass.CompassAccuracy
import io.trewartha.positional.data.compass.CompassAzimuth
import io.trewartha.positional.data.compass.CompassMode
import io.trewartha.positional.data.measurement.Angle
import io.trewartha.positional.data.ui.CompassNorthVibration
import io.trewartha.positional.domain.compass.CompassReading
import io.trewartha.positional.ui.NavDestination
import io.trewartha.positional.ui.PositionalTheme
import io.trewartha.positional.ui.bottomNavEnterTransition
import io.trewartha.positional.ui.bottomNavExitTransition
import io.trewartha.positional.ui.bottomNavPopEnterTransition
import io.trewartha.positional.ui.bottomNavPopExitTransition
import io.trewartha.positional.ui.locals.LocalVibrator
import io.trewartha.positional.ui.utils.placeholder

fun NavGraphBuilder.compassView(navController: NavController, contentPadding: PaddingValues) {
Expand All @@ -73,10 +77,16 @@ fun NavGraphBuilder.compassView(navController: NavController, contentPadding: Pa
) {
val viewModel: CompassViewModel = hiltViewModel()
val state by viewModel.state.collectAsStateWithLifecycle()
CompassView(
state = state,
contentPadding = contentPadding,
onHelpClick = { navController.navigate(NavDestination.CompassHelp.route) })
CompositionLocalProvider(
LocalVibrator provides
@Suppress("DEPRECATION") // It matches our needs and goes back pre API 21
LocalContext.current.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
) {
CompassView(
state = state,
contentPadding = contentPadding,
onHelpClick = { navController.navigate(NavDestination.CompassHelp.route) })
}
}
}

Expand Down Expand Up @@ -180,14 +190,17 @@ private fun SensorsPresentContent(
}
val context = LocalContext.current
val baseAzimuth = (state as? CompassViewModel.State.SensorsPresent.Loaded)?.let {
when (it.compassMode) {
CompassMode.MAGNETIC_NORTH -> it.compassReading.magneticAzimuth
CompassMode.TRUE_NORTH -> it.compassReading.trueAzimuth
when (it.mode) {
CompassMode.MAGNETIC_NORTH -> it.reading.magneticAzimuth
CompassMode.TRUE_NORTH -> it.reading.trueAzimuth
}?.angle
}
val adjustedAzimuth = baseAzimuth?.let { adjustAzimuthForDisplayRotation(context, it) }
val northVibration =
(state as? CompassViewModel.State.SensorsPresent.Loaded)?.northVibration
Compass(
adjustedAzimuth,
northVibration,
Modifier
.sizeIn(maxWidth = 480.dp, maxHeight = 480.dp)
.weight(1f)
Expand All @@ -197,7 +210,7 @@ private fun SensorsPresentContent(
verticalAlignment = Alignment.CenterVertically
) {
val declination = (state as? CompassViewModel.State.SensorsPresent.Loaded)
?.compassReading?.magneticDeclination?.inDegrees()?.value
?.reading?.magneticDeclination?.inDegrees()?.value
DeclinationText(declination)
HelpButton(onHelpClick)
}
Expand Down Expand Up @@ -290,15 +303,16 @@ private fun SensorsPresentLoadedPreview() {
Surface {
CompassView(
state = CompassViewModel.State.SensorsPresent.Loaded(
compassReading = CompassReading(
reading = CompassReading(
magneticAzimuth = CompassAzimuth(
angle = Angle.Degrees(40f),
accelerometerAccuracy = CompassAccuracy.HIGH,
magnetometerAccuracy = CompassAccuracy.MEDIUM
),
magneticDeclination = Angle.Degrees(5f)
),
compassMode = CompassMode.TRUE_NORTH
mode = CompassMode.TRUE_NORTH,
northVibration = CompassNorthVibration.SHORT
),
contentPadding = PaddingValues(),
onHelpClick = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.trewartha.positional.data.compass.CompassHardwareException
import io.trewartha.positional.data.compass.CompassMode
import io.trewartha.positional.data.settings.SettingsRepository
import io.trewartha.positional.data.ui.CompassNorthVibration
import io.trewartha.positional.domain.compass.CompassReading
import io.trewartha.positional.domain.compass.GetCompassReadingsUseCase
import io.trewartha.positional.ui.utils.flow.ForViewModel
Expand All @@ -23,11 +24,12 @@ class CompassViewModel @Inject constructor(
) : ViewModel() {

val state: StateFlow<State> =
combine<CompassReading, CompassMode, State>(
combine<CompassReading, CompassMode, CompassNorthVibration, State>(
getCompassReadingsUseCase(),
settingsRepository.compassMode
) { readings, mode ->
State.SensorsPresent.Loaded(readings, mode)
settingsRepository.compassMode,
settingsRepository.compassNorthVibration
) { reading, mode, northVibration ->
State.SensorsPresent.Loaded(reading, mode, northVibration)
}.catch { throwable ->
when (throwable) {
is CompassHardwareException -> {
Expand All @@ -52,8 +54,9 @@ class CompassViewModel @Inject constructor(
data object Loading : SensorsPresent

data class Loaded(
val compassReading: CompassReading,
val compassMode: CompassMode
val reading: CompassReading,
val mode: CompassMode,
val northVibration: CompassNorthVibration
) : SensorsPresent
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.trewartha.positional.ui.locals

import android.os.Vibrator
import androidx.compose.runtime.staticCompositionLocalOf

val LocalVibrator = staticCompositionLocalOf<Vibrator> {
error("No vibrator has been specified")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.trewartha.positional.ui.settings

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Vibration
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.trewartha.positional.R
import io.trewartha.positional.data.ui.CompassNorthVibration

@Composable
fun CompassNorthVibrationSetting(
value: CompassNorthVibration?,
onValueChange: (CompassNorthVibration) -> Unit,
modifier: Modifier = Modifier
) {
ListSetting(
icon = Icons.Rounded.Vibration,
title = stringResource(R.string.settings_compass_north_vibration_title),
values = CompassNorthVibration.entries.toSet(),
value = value,
valueName = { compassNorthVibration ->
stringResource(
when (compassNorthVibration) {
CompassNorthVibration.NONE ->
R.string.settings_compass_north_vibration_value_none
CompassNorthVibration.SHORT ->
R.string.settings_compass_north_vibration_value_short
CompassNorthVibration.MEDIUM ->
R.string.settings_compass_north_vibration_value_medium
CompassNorthVibration.LONG ->
R.string.settings_compass_north_vibration_value_long
}
)
},
valuesDialogTitle = stringResource(R.string.settings_compass_north_vibration_dialog_title),
valuesDialogText = null,
onValueChange = onValueChange,
modifier = modifier
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import io.trewartha.positional.R
import io.trewartha.positional.data.compass.CompassMode
import io.trewartha.positional.data.location.CoordinatesFormat
import io.trewartha.positional.data.measurement.Units
import io.trewartha.positional.data.ui.CompassNorthVibration
import io.trewartha.positional.data.ui.LocationAccuracyVisibility
import io.trewartha.positional.data.ui.Theme
import io.trewartha.positional.ui.NavDestination.Settings
Expand All @@ -56,6 +57,7 @@ fun NavGraphBuilder.settingsView(
) {
val viewModel: SettingsViewModel = hiltViewModel()
val compassMode by viewModel.compassMode.collectAsStateWithLifecycle()
val compassNorthVibration by viewModel.compassNorthVibration.collectAsStateWithLifecycle()
val coordinatesFormat by viewModel.coordinatesFormat.collectAsStateWithLifecycle()
val locationAccuracyVisibility by viewModel.locationAccuracyVisibility
.collectAsStateWithLifecycle()
Expand All @@ -64,6 +66,8 @@ fun NavGraphBuilder.settingsView(
SettingsView(
compassMode = compassMode,
onCompassModeChange = viewModel::onCompassModeChange,
compassNorthVibration = compassNorthVibration,
onCompassNorthVibrationChange = viewModel::onCompassNorthVibrationChange,
coordinatesFormat = coordinatesFormat,
onCoordinatesFormatChange = viewModel::onCoordinatesFormatChange,
locationAccuracyVisibility = locationAccuracyVisibility,
Expand All @@ -84,6 +88,8 @@ fun NavGraphBuilder.settingsView(
private fun SettingsView(
compassMode: CompassMode?,
onCompassModeChange: (CompassMode) -> Unit,
compassNorthVibration: CompassNorthVibration?,
onCompassNorthVibrationChange: (CompassNorthVibration) -> Unit,
coordinatesFormat: CoordinatesFormat?,
onCoordinatesFormatChange: (CoordinatesFormat) -> Unit,
locationAccuracyVisibility: LocationAccuracyVisibility?,
Expand Down Expand Up @@ -139,6 +145,11 @@ private fun SettingsView(
onValueChange = onCompassModeChange,
modifier = Modifier.fillMaxWidth()
)
CompassNorthVibrationSetting(
value = compassNorthVibration,
onValueChange = onCompassNorthVibrationChange,
modifier = Modifier.fillMaxWidth()
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LocationAccuracyVisibilitySetting(
value = locationAccuracyVisibility,
Expand Down Expand Up @@ -173,6 +184,8 @@ private fun LoadingPreviews() {
SettingsView(
compassMode = null,
onCompassModeChange = {},
compassNorthVibration = null,
onCompassNorthVibrationChange = {},
coordinatesFormat = null,
onCoordinatesFormatChange = {},
locationAccuracyVisibility = null,
Expand All @@ -195,6 +208,8 @@ private fun LoadedPreviews() {
SettingsView(
compassMode = CompassMode.TRUE_NORTH,
onCompassModeChange = {},
compassNorthVibration = CompassNorthVibration.SHORT,
onCompassNorthVibrationChange = {},
coordinatesFormat = CoordinatesFormat.DD,
onCoordinatesFormatChange = {},
locationAccuracyVisibility = LocationAccuracyVisibility.SHOW,
Expand Down
Loading