Geolocation and Device APIs
1. Geolocation API (getCurrentPosition, watchPosition)
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| getCurrentPosition | navigator.geolocation.getCurrentPosition(success, error, options) |
Gets current location once. Requires user permission. HTTPS required. | All Browsers |
| watchPosition | navigator.geolocation.watchPosition(success, error, options) |
Watches location changes continuously. Returns watch ID. Requires permission. | All Browsers |
| clearWatch | navigator.geolocation.clearWatch(id) |
Stops watching location. Use watch ID from watchPosition. | All Browsers |
| Position Property | Type | Description | Always Available |
|---|---|---|---|
| coords.latitude | number |
Latitude in decimal degrees (-90 to 90). | Yes |
| coords.longitude | number |
Longitude in decimal degrees (-180 to 180). | Yes |
| coords.accuracy | number |
Accuracy in meters. Lower is better. | Yes |
| coords.altitude | number | null |
Altitude in meters above sea level. May be null. |
No |
| coords.altitudeAccuracy | number | null |
Altitude accuracy in meters. May be null. |
No |
| coords.heading | number | null |
Direction of travel in degrees (0-360). 0 is north. May be null. |
No |
| coords.speed | number | null |
Speed in meters per second. May be null. |
No |
| timestamp | number |
DOMTimeStamp when position was acquired. | Yes |
| Option | Type | Description | Default |
|---|---|---|---|
| enableHighAccuracy | boolean |
Request GPS/high accuracy. Slower, more battery. Desktop may ignore. | false |
| timeout | number |
Max time in milliseconds to wait. 0 means no timeout. | Infinity |
| maximumAge | number |
Max age in milliseconds of cached position. 0 means no cache. | 0 |
| Error Code | Value | Meaning |
|---|---|---|
| PERMISSION_DENIED | 1 | User denied permission or location disabled. |
| POSITION_UNAVAILABLE | 2 | Location information unavailable (no GPS signal, network error). |
| TIMEOUT | 3 | Request timed out before getting location. |
Example: Get current location
// Check if geolocation is supported
if ("geolocation" in navigator) {
console.log("Geolocation available");
} else {
console.log("Geolocation not supported");
}
// Get current position
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
console.log(`Location: ${latitude}, ${longitude}`);
console.log(`Accuracy: ${accuracy} meters`);
console.log(`Timestamp: ${new Date(position.timestamp)}`);
// Optional fields
if (position.coords.altitude !== null) {
console.log(`Altitude: ${position.coords.altitude} meters`);
}
if (position.coords.speed !== null) {
console.log(`Speed: ${position.coords.speed} m/s`);
}
if (position.coords.heading !== null) {
console.log(`Heading: ${position.coords.heading} degrees`);
}
},
(error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
console.error("User denied geolocation permission");
break;
case error.POSITION_UNAVAILABLE:
console.error("Location unavailable");
break;
case error.TIMEOUT:
console.error("Geolocation timeout");
break;
default:
console.error("Unknown geolocation error");
}
},
{
"enableHighAccuracy": true,
"timeout": 10000,
"maximumAge": 0
}
);
Example: Watch position and distance calculation
let watchId;
// Start watching position
function startWatching() {
watchId = navigator.geolocation.watchPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
console.log(`Updated location: ${latitude}, ${longitude}`);
console.log(`Accuracy: ${accuracy}m`);
// Update map or UI
updateMap(latitude, longitude);
},
(error) => {
console.error("Watch position error:", error.message);
},
{
"enableHighAccuracy": true,
"timeout": 5000,
"maximumAge": 2000
}
);
console.log("Watching position, ID:", watchId);
}
// Stop watching
function stopWatching() {
if (watchId) {
navigator.geolocation.clearWatch(watchId);
console.log("Stopped watching position");
}
}
// Calculate distance between two points (Haversine formula)
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3; // Earth radius in meters
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
// Usage
const distance = calculateDistance(51.5074, -0.1278, 48.8566, 2.3522);
console.log(`Distance: ${(distance / 1000).toFixed(2)} km`);
Note: Geolocation requires HTTPS (or localhost for development). User must grant permission - request shows browser prompt. Accuracy varies by device (GPS, WiFi, IP).
enableHighAccuracy uses GPS on mobile (slower, more battery). Desktop typically uses IP/WiFi regardless.
Warning:
watchPosition can drain battery - always clearWatch() when done. Don't rely on optional fields (altitude, speed, heading) - may be null. Location can be spoofed - don't trust for security. Privacy concern - explain why you need location to users.
2. Device Orientation (deviceorientation, devicemotion)
| Event | Description | Browser Support |
|---|---|---|
| deviceorientation | Fires when device orientation changes. Provides alpha, beta, gamma rotation angles. | Mobile Browsers |
| deviceorientationabsolute | Like deviceorientation but relative to Earth's coordinate frame (compass). | Modern Mobile |
| devicemotion | Fires when device acceleration changes. Provides acceleration and rotation rate data. | Mobile Browsers |
| DeviceOrientation Property | Type | Description | Range |
|---|---|---|---|
| alpha | number |
Rotation around Z axis (compass direction). 0-360 degrees. | 0-360 |
| beta | number |
Rotation around X axis (front-to-back tilt). -180 to 180 degrees. | -180 to 180 |
| gamma | number |
Rotation around Y axis (left-to-right tilt). -90 to 90 degrees. | -90 to 90 |
| absolute | boolean |
true if orientation is absolute (compass-based), false if arbitrary. |
- |
| DeviceMotion Property | Type | Description | Units |
|---|---|---|---|
| acceleration | object |
Acceleration excluding gravity. Properties: x, y, z. | m/s² |
| accelerationIncludingGravity | object |
Acceleration including gravity. Properties: x, y, z. | m/s² |
| rotationRate | object |
Rotation speed. Properties: alpha, beta, gamma. | deg/s |
| interval | number |
Interval in milliseconds between events. | ms |
Example: Device orientation
// Request permission (iOS 13+)
async function requestOrientationPermission() {
if (typeof DeviceOrientationEvent !== "undefined" &&
typeof DeviceOrientationEvent.requestPermission === "function") {
try {
const permission = await DeviceOrientationEvent.requestPermission();
if (permission === "granted") {
console.log("Orientation permission granted");
return true;
}
} catch (error) {
console.error("Orientation permission error:", error);
}
return false;
}
return true; // No permission needed (Android, older iOS)
}
// Listen for orientation changes
window.addEventListener("deviceorientation", (event) => {
const alpha = event.alpha; // Compass direction (0-360)
const beta = event.beta; // Front-to-back tilt (-180 to 180)
const gamma = event.gamma; // Left-to-right tilt (-90 to 90)
const absolute = event.absolute;
console.log(`Alpha (Z): ${alpha?.toFixed(2)}°`);
console.log(`Beta (X): ${beta?.toFixed(2)}°`);
console.log(`Gamma (Y): ${gamma?.toFixed(2)}°`);
console.log(`Absolute: ${absolute}`);
// Rotate element based on device orientation
const element = document.querySelector(".compass");
if (alpha !== null) {
element.style.transform = `rotate(${-alpha}deg)`;
}
});
// Compass heading (absolute orientation)
window.addEventListener("deviceorientationabsolute", (event) => {
const heading = event.alpha; // True compass heading
console.log(`Compass heading: ${heading?.toFixed(2)}°`);
});
Example: Device motion and shake detection
// Request permission (iOS 13+)
async function requestMotionPermission() {
if (typeof DeviceMotionEvent !== "undefined" &&
typeof DeviceMotionEvent.requestPermission === "function") {
try {
const permission = await DeviceMotionEvent.requestPermission();
return permission === "granted";
} catch (error) {
console.error("Motion permission error:", error);
return false;
}
}
return true;
}
// Listen for motion events
window.addEventListener("devicemotion", (event) => {
// Acceleration (without gravity)
const acc = event.acceleration;
if (acc) {
console.log(`Acceleration: x=${acc.x}, y=${acc.y}, z=${acc.z} m/s²`);
}
// Acceleration (with gravity)
const accGrav = event.accelerationIncludingGravity;
if (accGrav) {
console.log(`With gravity: x=${accGrav.x}, y=${accGrav.y}, z=${accGrav.z}`);
}
// Rotation rate
const rotation = event.rotationRate;
if (rotation) {
console.log(`Rotation: α=${rotation.alpha}, β=${rotation.beta}, γ=${rotation.gamma} deg/s`);
}
console.log(`Interval: ${event.interval}ms`);
});
// Shake detection
let lastTime = 0;
let lastX = 0, lastY = 0, lastZ = 0;
const shakeThreshold = 15; // Adjust sensitivity
window.addEventListener("devicemotion", (event) => {
const acc = event.accelerationIncludingGravity;
if (!acc) return;
const currentTime = Date.now();
const timeDiff = currentTime - lastTime;
if (timeDiff > 100) {
const deltaX = Math.abs(acc.x - lastX);
const deltaY = Math.abs(acc.y - lastY);
const deltaZ = Math.abs(acc.z - lastZ);
const speed = (deltaX + deltaY + deltaZ) / timeDiff * 10000;
if (speed > shakeThreshold) {
console.log("Device shaken!");
onShake();
}
lastTime = currentTime;
lastX = acc.x;
lastY = acc.y;
lastZ = acc.z;
}
});
function onShake() {
console.log("Shake detected!");
// Trigger shake action
}
Note: iOS 13+ requires user permission and user gesture to access orientation/motion. Request permission with
DeviceOrientationEvent.requestPermission(). Events fire frequently (30-60 times/second) - throttle expensive operations. Desktop browsers may not support these events.
Warning: Sensor data can be noisy - apply smoothing/filtering for better results. Battery drain with continuous monitoring - remove listeners when not needed. Privacy concern - can be used for fingerprinting. Not all devices have all sensors (gyroscope, magnetometer).
3. Battery Status API (navigator.getBattery)
| Property | Type | Description | Status |
|---|---|---|---|
| navigator.getBattery() | Promise<BatteryManager> |
Returns Promise with battery status object. | DEPRECATED |
| level | number |
Battery level 0.0 to 1.0 (0% to 100%). | Read-only |
| charging | boolean |
true if device is charging. |
Read-only |
| chargingTime | number |
Seconds until fully charged. Infinity if not charging or unknown. |
Read-only |
| dischargingTime | number |
Seconds until battery empty. Infinity if charging or unknown. |
Read-only |
| Event | When Fired |
|---|---|
| levelchange | Battery level changed |
| chargingchange | Charging status changed (plugged/unplugged) |
| chargingtimechange | Charging time estimate changed |
| dischargingtimechange | Discharging time estimate changed |
Example: Battery status monitoring
// Check if API is available
if ("getBattery" in navigator) {
navigator.getBattery().then((battery) => {
// Initial state
console.log(`Battery level: ${battery.level * 100}%`);
console.log(`Charging: ${battery.charging}`);
console.log(`Charging time: ${battery.chargingTime} seconds`);
console.log(`Discharging time: ${battery.dischargingTime} seconds`);
// Listen for level changes
battery.addEventListener("levelchange", () => {
console.log(`Battery level: ${battery.level * 100}%`);
// Warn if low battery
if (battery.level < 0.2 && !battery.charging) {
console.warn("Low battery!");
}
});
// Listen for charging state changes
battery.addEventListener("chargingchange", () => {
if (battery.charging) {
console.log("Device is charging");
} else {
console.log("Device is not charging");
}
});
// Charging time updates
battery.addEventListener("chargingtimechange", () => {
if (battery.chargingTime !== Infinity) {
const minutes = Math.floor(battery.chargingTime / 60);
console.log(`Fully charged in ${minutes} minutes`);
}
});
// Discharging time updates
battery.addEventListener("dischargingtimechange", () => {
if (battery.dischargingTime !== Infinity) {
const minutes = Math.floor(battery.dischargingTime / 60);
console.log(`Battery empty in ${minutes} minutes`);
}
});
});
} else {
console.log("Battery Status API not supported");
}
Warning: Battery Status API is DEPRECATED due to privacy concerns (fingerprinting). Removed from most browsers. Firefox disabled by default. Chrome plans removal. Don't rely on this API for new projects. Use feature detection before accessing.
4. Vibration API (navigator.vibrate)
| Method | Syntax | Description | Browser Support |
|---|---|---|---|
| vibrate | navigator.vibrate(pattern) |
Vibrates device with pattern. Pattern is number (ms) or array [vibrate, pause, ...]. | Mobile Browsers |
| vibrate(0) | navigator.vibrate(0) |
Stops ongoing vibration. | Mobile Browsers |
Example: Vibration patterns
// Check support
if ("vibrate" in navigator) {
console.log("Vibration API supported");
} else {
console.log("Vibration not supported");
}
// Simple vibration (200ms)
navigator.vibrate(200);
// Vibration pattern [vibrate, pause, vibrate, pause, ...]
// Vibrate 200ms, pause 100ms, vibrate 200ms
navigator.vibrate([200, 100, 200]);
// SOS pattern (... --- ...)
navigator.vibrate([
100, 30, 100, 30, 100, // S (...)
200, // Pause
200, 30, 200, 30, 200, // O (---)
200, // Pause
100, 30, 100, 30, 100 // S (...)
]);
// Stop vibration
navigator.vibrate(0);
// or
navigator.vibrate([]);
// Pulse pattern (heartbeat)
function vibratePulse() {
navigator.vibrate([50, 50, 50, 50, 200]);
}
// Success feedback
function vibrateSuccess() {
navigator.vibrate([100, 50, 100]);
}
// Error feedback
function vibrateError() {
navigator.vibrate([200, 100, 200, 100, 200]);
}
// Click feedback
function vibrateClick() {
navigator.vibrate(10); // Short tap
}
// Button with haptic feedback
document.querySelector("button").addEventListener("click", () => {
navigator.vibrate(50);
// Handle button action
});
Note: Vibration only works on mobile devices with vibration hardware. Desktop browsers ignore vibrate() calls. Maximum vibration time limited by browsers (few seconds). User can disable vibration in system settings. Requires user gesture on some browsers.
Warning: Don't overuse vibration - annoying and drains battery. Respect user preferences - provide option to disable. Some users rely on vibration for accessibility - use meaningfully. Vibration patterns limited to prevent abuse (max length, max duration).
5. Screen Orientation (screen.orientation.lock)
| Property/Method | Description | Browser Support |
|---|---|---|
| screen.orientation.type | Current orientation: "portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary". | Modern Browsers |
| screen.orientation.angle | Orientation angle: 0, 90, 180, or 270 degrees. | Modern Browsers |
| screen.orientation.lock(orientation) | Locks to specific orientation. Returns Promise. Requires fullscreen on most browsers. | Modern Mobile |
| screen.orientation.unlock() | Unlocks orientation, allowing device rotation. | Modern Mobile |
| orientationchange event | Fires when screen orientation changes. | Modern Browsers |
| Orientation Value | Description |
|---|---|
| portrait-primary | Normal portrait (0° or 180°) |
| portrait-secondary | Upside-down portrait (180° or 0°) |
| landscape-primary | Normal landscape (90° or 270°) |
| landscape-secondary | Reverse landscape (270° or 90°) |
| portrait | Any portrait orientation |
| landscape | Any landscape orientation |
| any | Allow all orientations (default) |
Example: Detect and lock orientation
// Check current orientation
if (screen.orientation) {
console.log(`Type: ${screen.orientation.type}`);
console.log(`Angle: ${screen.orientation.angle}°`);
} else {
console.log("Screen Orientation API not supported");
}
// Listen for orientation changes
screen.orientation?.addEventListener("change", () => {
console.log(`Orientation changed to: ${screen.orientation.type}`);
console.log(`Angle: ${screen.orientation.angle}°`);
// Adjust layout based on orientation
if (screen.orientation.type.startsWith("landscape")) {
document.body.classList.add("landscape");
document.body.classList.remove("portrait");
} else {
document.body.classList.add("portrait");
document.body.classList.remove("landscape");
}
});
// Lock orientation (requires fullscreen)
async function lockLandscape() {
try {
// Enter fullscreen first
await document.documentElement.requestFullscreen();
// Lock to landscape
await screen.orientation.lock("landscape");
console.log("Locked to landscape");
} catch (error) {
console.error("Failed to lock orientation:", error);
}
}
// Lock to specific orientation
async function lockOrientation(orientation) {
try {
await screen.orientation.lock(orientation);
console.log(`Locked to ${orientation}`);
} catch (error) {
if (error.name === "NotSupportedError") {
console.error("Orientation lock not supported");
} else if (error.name === "SecurityError") {
console.error("Must be in fullscreen to lock orientation");
} else {
console.error("Lock failed:", error);
}
}
}
// Unlock orientation
function unlockOrientation() {
screen.orientation?.unlock();
console.log("Orientation unlocked");
}
// Game example: lock to landscape when playing
async function startGame() {
await document.documentElement.requestFullscreen();
await screen.orientation.lock("landscape");
// Start game
}
function exitGame() {
screen.orientation.unlock();
document.exitFullscreen();
}
Note: Orientation lock typically requires fullscreen mode. Desktop browsers may not support orientation lock. Use CSS media queries for responsive design alongside API. Some browsers use
window.orientation (deprecated) - use screen.orientation instead.
Warning: Don't lock orientation unless necessary (games, video). Respect user preference - they rotated device intentionally. Unlock orientation when leaving fullscreen. May not work in iframes or certain contexts.
6. Sensor APIs (Accelerometer, Gyroscope)
| Sensor | Description | Browser Support |
|---|---|---|
| Accelerometer | Measures acceleration in m/s² along X, Y, Z axes. | Limited (Chrome) |
| LinearAccelerationSensor | Acceleration excluding gravity. | Limited (Chrome) |
| GravitySensor | Gravity component of acceleration. | Limited (Chrome) |
| Gyroscope | Measures angular velocity (rotation rate) in rad/s. | Limited (Chrome) |
| Magnetometer | Measures magnetic field in µT (microtesla). | Limited (Chrome) |
| AbsoluteOrientationSensor | Device orientation relative to Earth's coordinate system. | Limited (Chrome) |
| RelativeOrientationSensor | Device orientation relative to arbitrary reference. | Limited (Chrome) |
| AmbientLightSensor | Measures ambient light in lux. | Limited (Chrome) |
| Sensor Method/Property | Description |
|---|---|
| start() | Starts sensor. Fires reading event when data available. |
| stop() | Stops sensor and data collection. |
| x, y, z | Sensor readings for respective axes. |
| timestamp | DOMHighResTimeStamp when reading was taken. |
| activated | true if sensor is active and providing readings. |
| reading event | Fires when new sensor reading is available. |
| error event | Fires when sensor error occurs. |
Example: Accelerometer usage
// Request permission (if needed)
async function requestSensorPermission() {
try {
const result = await navigator.permissions.query({ "name": "accelerometer" });
if (result.state === "denied") {
console.error("Accelerometer permission denied");
return false;
}
return true;
} catch (error) {
console.error("Permission query failed:", error);
return false;
}
}
// Create accelerometer
try {
const accelerometer = new Accelerometer({
"frequency": 60 // 60 Hz (readings per second)
});
// Listen for readings
accelerometer.addEventListener("reading", () => {
console.log(`Acceleration: x=${accelerometer.x}, y=${accelerometer.y}, z=${accelerometer.z} m/s²`);
console.log(`Timestamp: ${accelerometer.timestamp}`);
// Detect device tilt
const tiltX = Math.atan2(accelerometer.y, accelerometer.z) * 180 / Math.PI;
const tiltY = Math.atan2(accelerometer.x, accelerometer.z) * 180 / Math.PI;
console.log(`Tilt: X=${tiltX.toFixed(2)}°, Y=${tiltY.toFixed(2)}°`);
});
// Handle errors
accelerometer.addEventListener("error", (event) => {
console.error("Accelerometer error:", event.error.name, event.error.message);
});
// Start sensor
accelerometer.start();
console.log("Accelerometer started");
// Stop after 10 seconds
setTimeout(() => {
accelerometer.stop();
console.log("Accelerometer stopped");
}, 10000);
} catch (error) {
console.error("Accelerometer not supported:", error);
}
// Linear acceleration (without gravity)
try {
const linearAccel = new LinearAccelerationSensor({ "frequency": 60 });
linearAccel.addEventListener("reading", () => {
console.log(`Linear acceleration: x=${linearAccel.x}, y=${linearAccel.y}, z=${linearAccel.z}`);
});
linearAccel.start();
} catch (error) {
console.error("LinearAccelerationSensor not supported:", error);
}
Example: Gyroscope and orientation sensors
// Gyroscope (rotation rate)
try {
const gyroscope = new Gyroscope({ "frequency": 60 });
gyroscope.addEventListener("reading", () => {
console.log(`Rotation rate: x=${gyroscope.x}, y=${gyroscope.y}, z=${gyroscope.z} rad/s`);
// Convert to degrees per second
const degPerSec = {
"x": gyroscope.x * 180 / Math.PI,
"y": gyroscope.y * 180 / Math.PI,
"z": gyroscope.z * 180 / Math.PI
};
console.log(`Rotation (deg/s): x=${degPerSec.x.toFixed(2)}, y=${degPerSec.y.toFixed(2)}, z=${degPerSec.z.toFixed(2)}`);
});
gyroscope.start();
} catch (error) {
console.error("Gyroscope not supported:", error);
}
// Absolute orientation (quaternion)
try {
const orientation = new AbsoluteOrientationSensor({ "frequency": 60 });
orientation.addEventListener("reading", () => {
// Quaternion representation
const [x, y, z, w] = orientation.quaternion;
console.log(`Quaternion: [${x}, ${y}, ${z}, ${w}]`);
// Convert to Euler angles
const angles = quaternionToEuler(x, y, z, w);
console.log(`Euler angles: ${JSON.stringify(angles)}`);
});
orientation.start();
} catch (error) {
console.error("AbsoluteOrientationSensor not supported:", error);
}
// Helper: Convert quaternion to Euler angles
function quaternionToEuler(x, y, z, w) {
const roll = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));
const pitch = Math.asin(2 * (w * y - z * x));
const yaw = Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z));
return {
"roll": roll * 180 / Math.PI,
"pitch": pitch * 180 / Math.PI,
"yaw": yaw * 180 / Math.PI
};
}
// Ambient light sensor
try {
const lightSensor = new AmbientLightSensor({ "frequency": 1 });
lightSensor.addEventListener("reading", () => {
console.log(`Illuminance: ${lightSensor.illuminance} lux`);
// Adjust UI based on ambient light
if (lightSensor.illuminance < 50) {
document.body.classList.add("dark-mode");
} else {
document.body.classList.remove("dark-mode");
}
});
lightSensor.start();
} catch (error) {
console.error("AmbientLightSensor not supported:", error);
}
Note: Generic Sensor API has limited browser support (mainly Chrome). Requires HTTPS and user permission. Sensors may not be available on all devices. Use
frequency option to control sampling rate (affects battery and CPU). Always wrap in try/catch for feature detection.
Warning: Sensor APIs can drain battery with high frequency - use lowest frequency needed. Always
stop() sensors when not in use. Privacy concern - sensor data can be used for fingerprinting or side-channel attacks. Check browser compatibility before using - very limited support.
Device and Sensor API Best Practices
- Always check feature availability with
if ("geolocation" in navigator)before using APIs - Request location/sensor permissions with clear explanation why you need them
- Use
enableHighAccuracy: falsefor geolocation unless GPS precision is critical - Always
clearWatch()geolocation watches when done to save battery - Throttle or debounce orientation/motion event handlers - they fire very frequently
- Request DeviceOrientation/Motion permissions on iOS 13+ with user gesture
- Don't rely on Battery Status API - deprecated and removed from most browsers
- Use vibration sparingly - annoying when overused and drains battery
- Orientation lock requires fullscreen mode - unlock when exiting fullscreen
- Generic Sensor API has limited support - use DeviceOrientation/Motion for broader compatibility
- Always stop sensors when not needed to conserve battery and CPU
- Handle permission denials gracefully - provide fallback UX