Blog Infos
Author
Published
Topics
, , , ,
Published

Android now fuses more sensors, so you do not have to do calculations on your own.

It gives you the bearing and orientation of your device.

In specific, the new Orientation device fuses the accelerometer, gyroscope and magnetometer to bring more precise and consistent measurements of the device’s orientation. It compensates for sensor issues on the lower levels and timing of the sensors.

Implementation
Dependencies

Firstly, you need to update Google Play location services to the version 21.2.0:

Version catalogue:

[versions]
playServicesLocation = "21.2.0"

[libraries]
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }

gradle.kts:

dependencies {
    implementation('com.google.android.gms:play-services-location:21.2.0')
    // Or with toml:
    implementation(libs.play.services.location)
}

No registration for Google Services API is required as for other sensors.

Implementation

Registration follows a similar workflow as any other sensor from the API. The fused API is presented with one provider client and one listener.

The client can be obtained by no activity context as follows:

private val fusedOrientationProviderClient: FusedOrientationProviderClient =
    LocationServices.getFusedOrientationProviderClient(context)

If you want to start listening to the changes:

  1. Create a request with your desired latency
  2. create or pass Executor. It is a new Thread, so if you have other similar jobs around, they should share it.
  3. Pass the listener, executor and request.
  4. Listen to success or failure.
// 1. 
val request = 
    DeviceOrientationRequest.Builder(DeviceOrientationRequest.OUTPUT_PERIOD_DEFAULT).build()
// 2. 
val executor = Executors.newSingleThreadExecutor()
// 3.
fusedOrientationProviderClient.requestOrientationUpdates(request, executor, listener)
    // 4.
    .addOnSuccessListener {}
    .addOnFailureListener { e: Exception? -> ...}

When you want to stop the sensor, you remove the registered listener:

fusedOrientationProviderClient.removeOrientationUpdates(listener)

For listening, you have to implement the interface you could add lambda:

{ sample: DeviceOrientation -> ...}

Or you can have any other class to implement the DeviceOrientationListener, which provided the needed method to listen.

Speed of the sensor:

  • It does not need any permission to run — max frequency is capped at 200Hz (Android S and higher)
  • Higher frequencies require permission — android.permissions.HIGH_SAMPLING_RATE_SENSORS

Frequency can be modulated by the builder parameter:

  • DeviceOrientationRequest.OUTPUT_PERIOD_DEFAULT – 50Hz / 20ms period – recommended for map or compass
  • DeviceOrientationRequest.OUTPUT_PERIOD_MEDIUM– 100Hz / 10ms period – recommended gesture detection
  • DeviceOrientationRequest.OUTPUT_PERIOD_FAST – 200Hz / 5ms period – recommended for AR apps
  • Service provides samples only if the app is in the foreground.

Android system does not provide samples in a way you might expect. The 5/10/20ms means, that system should supply the sample by that time. Not that the system will give you sample every 20ms. Every device can be a bit different.

Output of the listener — DeviceOrientation
  • getAttitude() — represents the 3D orientation of the phone relative to the east-north-up coordinate frame
  • getConservativeHeadingErrorDegrees() — heading error in degrees — [0, 180] — calculated from more samples
  • hasConservativeHeadingErrorDegrees() — if the error above is available
  • getHeading() — heading of the device or “bearing of the compass”
  • getHeadingErrorDegrees() — heading error in degrees — [0, 180]
  • getElapsedRealtimeNs() — nanoseconds from the boot of the device
Rotation matrix

Previously, you would need measurements from the accelerometer and gyroscope, to get the rotation matrix and afterwards orientation angles:

val rotationMatrix = FloatArray(9)
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading)

val orientationAngles = FloatArray(3)
SensorManager.getOrientation(rotationMatrix, orientationAngles)

Now, you can calculate the rotation matrix directly with the help of the DeviceOrienation attitude:

val rotationMatrix = FloatArray(9)
SensorManager.getRotationMatrixFromVector(rotationMatrix, deviceOrientation.getAttitude())
Repo example

If you are looking for some quick and simple sample code to implement the fused orientation API, here is a small sample:

// limitation for one listener, but it could be extended with array of listeners
class OrientationRepo(context: Context) {
    private var listener: DeviceOrientationListener? = null
    private val fusedOrientationProviderClient: FusedOrientationProviderClient =
        LocationServices.getFusedOrientationProviderClient(context)
    fun addListener(
        listener: DeviceOrientationListener,
        executor: ExecutorService = Executors.newSingleThreadExecutor()
    ) {
        // if we register second listener, we replace the first one
        removeListenerIfExists()
        this.listener = listener
        
        // feel free to extract period as parameter
        val request =
            DeviceOrientationRequest.Builder(DeviceOrientationRequest.OUTPUT_PERIOD_DEFAULT).build()
        
        // success and failure listeners could be added as input parameters for callback logic
        fusedOrientationProviderClient.requestOrientationUpdates(request, executor, listener)
            .addOnSuccessListener {
                Log.i(TAG, "Successfully added new orientation listener")
            }.addOnFailureListener { e: Exception? ->
                Log.e(TAG, "Failed to add new orientation listener", e)
            }
    }
    fun removeListenerIfExists() = listener?.let {
        Log.i(TAG, "Removing active orientation listener")
        fusedOrientationProviderClient.removeOrientationUpdates(it)
        listener = null
    }
    companion object {
        const val TAG = "OrientationRepo"
    }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

Orient the map with a compass

Here is a small example of rotating the map with the help of Jetpack Compose, hilt and repository above.

This is not a tutorial for the implementation of GoogleMap or Hilt, you can visit the documentation on how to implement GoogleMap here and my article about Hilt implementation:

https://tomasrepcik.dev/blog/2023/2023-05-25-android-hilt/?source=post_page—–dc4e5c25ca35——————————–

Jetpack Compose version of GoogleMap UI is here.

So let’s introduce the main screen, where we will tap into a single ViewModel. The model emits the CameraPositionState, with our new bearing from the repo.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MapViewFusedOrientationApiExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
                ) {
                    val orientationViewModel: OrientationViewModel = hiltViewModel()
                    OrientationUi(state = orientationViewModel.cameraPositionState.collectAsState().value,
                        onStart = { orientationViewModel.start() },
                        onStop = { orientationViewModel.stop() })
                }
            }
        }
    }
}

OrientationViewModel contains our Orientation repo and it starts based on events coming from the UI.

The samples will be emitted on different thread then the Main thread. That is why, it is required to switch context to update UI. Otherwise, you hit error.

@HiltViewModel
class OrientationViewModel @Inject constructor(@ApplicationContext context: Context) : ViewModel(),
    DeviceOrientationListener { // implements the orientation listener
    // repo from the implementation
    private val orientationRepo = OrientationRepo(context)
    private val _cameraPositionState: MutableStateFlow<CameraPositionState> = MutableStateFlow(
        CameraPositionState()
    )
    val cameraPositionState = _cameraPositionState.asStateFlow()
    // to start and stop the samples
    fun start() = orientationRepo.addListener(this)
    fun stop() = orientationRepo.removeListenerIfExists()
    override fun onDeviceOrientationChanged(orientation: DeviceOrientation) {
        // launcher coroutine on the main thread and our sample
        viewModelScope.launch(Dispatchers.Main) {
            _cameraPositionState.value = CameraPositionState(CameraPosition.builder().apply {
                target(LatLng(49.06144, 20.29798)) // could be extended with GPS implementation
                bearing(orientation.headingDegrees) // getting our heading / bearing as compass
                zoom(10f)
            }.build())
        }
    }
}

Here is the implementation for the GoogleMap UI with the side effects. We need to use LaunchedEffect to start the listener when the UI appears. Afterwards, LifecycleOwner takes care of turning it off and on based on the lifecycle of the app (Orientation API works only in the foreground). DisposableEffect cleans everything up.

@Composable
fun OrientationUi(
    state: CameraPositionState,
    onStart: () -> Unit,
    onStop: () -> Unit,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
) {
    // first start
    LaunchedEffect(lifecycleOwner) {
        onStart()
    }
    DisposableEffect(lifecycleOwner) {
        // response to the lifecycle changes
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                onStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                onStop()
            }
        }
        // clean up
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
    GoogleMap(
        modifier = Modifier.fillMaxSize(),
        cameraPositionState = state, // our state 
        uiSettings = MapUiSettings(compassEnabled = false)
    )
}

After putting it together, the map should rotate based on your device bearing as in the gif below. In other words, where you point your device, the map will orient itself to the device.

The full example repository is:

https://github.com/Foxpace/MapView-FusedOrientationApi-Example?source=post_page—–dc4e5c25ca35——————————–

Thanks for reading and do not forget to follow for more!

Resources:

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu