/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.glass.sample.compass;
import com.google.android.glass.sample.compass.util.MathUtils;
import android.content.Context;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Collects and communicates information about the user's current orientation and location.
*/
public class OrientationManager {
/**
* The minimum distance desired between location notifications.
*/
private static final long METERS_BETWEEN_LOCATIONS = 2;
/**
* The minimum elapsed time desired between location notifications.
*/
private static final long MILLIS_BETWEEN_LOCATIONS = TimeUnit.SECONDS.toMillis(3);
/**
* The maximum age of a location retrieved from the passive location provider before it is
* considered too old to use when the compass first starts up.
*/
private static final long MAX_LOCATION_AGE_MILLIS = TimeUnit.MINUTES.toMillis(30);
/**
* The sensors used by the compass are mounted in the movable arm on Glass. Depending on how
* this arm is rotated, it may produce a displacement ranging anywhere from 0 to about 12
* degrees. Since there is no way to know exactly how far the arm is rotated, we just split the
* difference.
*/
private static final int ARM_DISPLACEMENT_DEGREES = 6;
/**
* Classes should implement this interface if they want to be notified of changes in the user's
* location, orientation, or the accuracy of the compass.
*/
public interface OnChangedListener {
/**
* Called when the user's orientation changes.
*
* @param orientationManager the orientation manager that detected the change
*/
void onOrientationChanged(OrientationManager orientationManager);
/**
* Called when the user's location changes.
*
* @param orientationManager the orientation manager that detected the change
*/
void onLocationChanged(OrientationManager orientationManager);
/**
* Called when the accuracy of the compass changes.
*
* @param orientationManager the orientation manager that detected the change
*/
void onAccuracyChanged(OrientationManager orientationManager);
}
private final SensorManager mSensorManager;
private final LocationManager mLocationManager;
private final Set<OnChangedListener> mListeners;
private final float[] mRotationMatrix;
private final float[] mOrientation;
private boolean mTracking;
private float mHeading;
private float mPitch;
private Location mLocation;
private GeomagneticField mGeomagneticField;
private boolean mHasInterference;
/**
* The sensor listener used by the orientation manager.
*/
private SensorEventListener mSensorListener = new SensorEventListener() {
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
if (sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mHasInterference = (accuracy < SensorManager.SENSOR_STATUS_ACCURACY_HIGH);
notifyAccuracyChanged();
}
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
// Get the current heading from the sensor, then notify the listeners of the
// change.
SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values);
SensorManager.remapCoordinateSystem(mRotationMatrix, SensorManager.AXIS_X,
SensorManager.AXIS_Z, mRotationMatrix);
SensorManager.getOrientation(mRotationMatrix, mOrientation);
// Store the pitch (used to display a message indicating that the user's head
// angle is too steep to produce reliable results.
mPitch = (float) Math.toDegrees(mOrientation[1]);
// Convert the heading (which is relative to magnetic north) to one that is
// relative to true north, using the user's current location to compute this.
float magneticHeading = (float) Math.toDegrees(mOrientation[0]);
mHeading = MathUtils.mod(computeTrueNorth(magneticHeading), 360.0f)
- ARM_DISPLACEMENT_DEGREES;
notifyOrientationChanged();
}
}
};
/**
* The location listener used by the orientation manager.
*/
private LocationListener mLocationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
mLocation = location;
updateGeomagneticField();
notifyLocationChanged();
}
@Override
public void onProviderDisabled(String provider) {
// Don't need to do anything here.
}
@Override
public void onProviderEnabled(String provider) {
// Don't need to do anything here.
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
// Don't need to do anything here.
}
};
/**
* Initializes a new instance of {@code OrientationManager}, using the specified context to
* access system services.
*/
public OrientationManager(SensorManager sensorManager, LocationManager locationManager) {
mRotationMatrix = new float[16];
mOrientation = new float[9];
mSensorManager = sensorManager;
mLocationManager = locationManager;
mListeners = new LinkedHashSet<OnChangedListener>();
}
/**
* Adds a listener that will be notified when the user's location or orientation changes.
*/
public void addOnChangedListener(OnChangedListener listener) {
mListeners.add(listener);
}
/**
* Removes a listener from the list of those that will be notified when the user's location or
* orientation changes.
*/
public void removeOnChangedListener(OnChangedListener listener) {
mListeners.remove(listener);
}
/**
* Starts tracking the user's location and orientation. After calling this method, any
* {@link OnChangedListener}s added to this object will be notified of these events.
*/
public void start() {
if (!mTracking) {
mSensorManager.registerListener(mSensorListener,
mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR),
SensorManager.SENSOR_DELAY_UI);
// The rotation vector sensor doesn't give us accuracy updates, so we observe the
// magnetic field sensor solely for those.
mSensorManager.registerListener(mSensorListener,
mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
SensorManager.SENSOR_DELAY_UI);
Location lastLocation = mLocationManager
.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
if (lastLocation != null) {
long locationAge = lastLocation.getTime() - System.currentTimeMillis();
if (locationAge < MAX_LOCATION_AGE_MILLIS) {
mLocation = lastLocation;
updateGeomagneticField();
}
}
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setBearingRequired(false);
criteria.setSpeedRequired(false);
List<String> providers =
mLocationManager.getProviders(criteria, true /* enabledOnly */);
for (String provider : providers) {
mLocationManager.requestLocationUpdates(provider,
MILLIS_BETWEEN_LOCATIONS, METERS_BETWEEN_LOCATIONS, mLocationListener,
Looper.getMainLooper());
}
mTracking = true;
}
}
/**
* Stops tracking the user's location and orientation. Listeners will no longer be notified of
* these events.
*/
public void stop() {
if (mTracking) {
mSensorManager.unregisterListener(mSensorListener);
mLocationManager.removeUpdates(mLocationListener);
mTracking = false;
}
}
/**
* Gets a value indicating whether there is too much magnetic field interference for the
* compass to be reliable.
*
* @return true if there is magnetic interference, otherwise false
*/
public boolean hasInterference() {
return mHasInterference;
}
/**
* Gets a value indicating whether the orientation manager knows the user's current location.
*
* @return true if the user's location is known, otherwise false
*/
public boolean hasLocation() {
return mLocation != null;
}
/**
* Gets the user's current heading, in degrees. The result is guaranteed to be between 0 and
* 360.
*
* @return the user's current heading, in degrees
*/
public float getHeading() {
return mHeading;
}
/**
* Gets the user's current pitch (head tilt angle), in degrees. The result is guaranteed to be
* between -90 and 90.
*
* @return the user's current pitch angle, in degrees
*/
public float getPitch() {
return mPitch;
}
/**
* Gets the user's current location.
*
* @return the user's current location
*/
public Location getLocation() {
return mLocation;
}
/**
* Notifies all listeners that the user's orientation has changed.
*/
private void notifyOrientationChanged() {
for (OnChangedListener listener : mListeners) {
listener.onOrientationChanged(this);
}
}
/**
* Notifies all listeners that the user's location has changed.
*/
private void notifyLocationChanged() {
for (OnChangedListener listener : mListeners) {
listener.onLocationChanged(this);
}
}
/**
* Notifies all listeners that the compass's accuracy has changed.
*/
private void notifyAccuracyChanged() {
for (OnChangedListener listener : mListeners) {
listener.onAccuracyChanged(this);
}
}
/**
* Updates the cached instance of the geomagnetic field after a location change.
*/
private void updateGeomagneticField() {
mGeomagneticField = new GeomagneticField((float) mLocation.getLatitude(),
(float) mLocation.getLongitude(), (float) mLocation.getAltitude(),
mLocation.getTime());
}
/**
* Use the magnetic field to compute true (geographic) north from the specified heading
* relative to magnetic north.
*
* @param heading the heading (in degrees) relative to magnetic north
* @return the heading (in degrees) relative to true north
*/
private float computeTrueNorth(float heading) {
if (mGeomagneticField != null) {
return heading + mGeomagneticField.getDeclination();
} else {
return heading;
}
}
}