A practical walkthrough — from setting up Google Maps and requesting permissions to running a background foreground service that triggers an alarm when you arrive at or leave a destination. Based on the real-world InOutAlarm app.
In this article
- What we are building
- Project setup & dependencies
- Declaring and requesting permissions
- Embedding Google Maps
- Getting the device location
- Setting a destination and drawing a geofence
- Background tracking with a Foreground Service
- Checking the alarm condition and triggering the alarm
- Posting notifications
- Managing app state
- Tips, gotchas & best practices
1. What We Are Building
A geofencing alarm app that monitors the user's GPS position in real time and fires an alarm — vibration, sound, and a push notification — the moment they either enter or leave a radius around a map pin they dropped.
Think of it as a smarter alarm clock: instead of going off at a time, it goes off at a place. Common uses include waking up on public transport, reminding yourself to get off at the right stop, or alerting you when you've wandered too far from a meeting point.
Core features we will cover:
- Embedding a live Google Map and letting the user long-press to drop a destination pin
- Drawing a adjustable-radius circle around the pin
- Obtaining the device's precise GPS location both in the foreground and in the background
- Running a Foreground Service so tracking continues when the screen is off
- Calculating distance to the destination and triggering an alarm when the threshold is crossed
- Posting high-priority notifications that appear on the lock screen
- Handling Android 10+ background location permission and the mandatory disclosure dialog
User drops pin User taps Start
│ │
▼ ▼
[Destination set] [Foreground Service starts]
│
GPS fix every 1 second
│
distance to destination?
┌────────┴────────┐
< radius > radius
(Arrival) (Leaving)
│ │
ALARM ◀─────────────────┘
vibrate + sound + notification2. Project Setup & Dependencies
Create the project
In Android Studio, choose New Project → Google Maps Activity. This scaffolds the map fragment and the API key placeholder for you automatically.
app/build.gradle
Set a minimum SDK of 26 (Android 8.0) so you can use VibrationEffect, NotificationChannel, and the modern PendingIntent.FLAG_IMMUTABLE flag without workarounds.
android {
compileSdkVersion 35
defaultConfig {
applicationId "com.example.locationalarm"
minSdkVersion 26 // Android 8.0 — required for NotificationChannel
targetSdkVersion 35
versionCode 1
versionName "1.0"
}
}
dependencies {
// Google Maps + Fused Location Provider
implementation 'com.google.android.gms:play-services-maps:19.0.0'
implementation 'com.google.android.gms:play-services-location:21.3.0'
// AndroidX
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.fragment:fragment:1.8.1'
}Google Maps API key
Go to the Google Cloud Console, enable the Maps SDK for Android, create an API key restricted to your app's package name and signing certificate, then paste it in res/values/google_maps_api.xml:
<resources>
<string name="google_maps_key" translatable="false">YOUR_API_KEY_HERE</string>
</resources>google_maps_api.xml to .gitignore, or use Android Secrets Gradle Plugin to load it from a local local.properties file.3. Declaring and Requesting Permissions
Declare in AndroidManifest.xml
AndroidManifest.xml<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Also mark your service with the foregroundServiceType attribute:
<service
android:name=".TrackLocationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />Request at runtime
Location and notification permissions must be requested at runtime. Do it in two stages:
private static final int REQUEST_CODE = 200;
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{ Manifest.permission.ACCESS_FINE_LOCATION },
REQUEST_CODE);
}AlertDialog that explains exactly why background location is needed, then request it only if the user agrees.MapsActivity.javaprivate void requestBackgroundLocationWithDisclosure() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return;
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_BACKGROUND_LOCATION)
== PackageManager.PERMISSION_GRANTED) return;
new AlertDialog.Builder(this)
.setTitle("Allow all-the-time location for trip alarm")
.setMessage(
"This app collects location data to monitor your distance to the " +
"destination and trigger an arrival or leaving alarm, even when " +
"the app is closed or the screen is off. Your location data stays " +
"on your device and is never shared with third parties.")
.setCancelable(false)
.setPositiveButton("Continue to permission", (d, w) ->
ActivityCompat.requestPermissions(this,
new String[]{ Manifest.permission.ACCESS_BACKGROUND_LOCATION },
REQUEST_CODE_BACKGROUND_LOCATION))
.setNegativeButton("Not now", null)
.show();
}onCreate.MapsActivity.javaprivate void requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{ Manifest.permission.POST_NOTIFICATIONS },
REQUEST_CODE);
}
}
}onRequestPermissionsResult() to chain permission requests. After fine location is granted, immediately check for background location if the user intends to start monitoring.4. Embedding Google Maps
Layout — FrameLayout overlay pattern
Use a FrameLayout as the root so that control buttons float on top of the map.
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Full-screen map behind everything -->
<fragment
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Control panel floating on top-left -->
<GridLayout
android:layout_gravity="start|top"
android:layout_marginTop="112dp"
android:columnCount="2"
... />
</FrameLayout>Initialise the map in code
MapsActivity.javapublic class MapsActivity extends FragmentActivity
implements OnMapReadyCallback, GoogleMap.OnCameraIdleListener {
private GoogleMap mMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);
SupportMapFragment mapFragment = (SupportMapFragment)
getSupportFragmentManager().findFragmentById(R.id.map);
mapFragment.getMapAsync(this); // calls onMapReady when ready
}
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mMap.setMyLocationEnabled(true); // blue dot for current position
mMap.getUiSettings().setZoomControlsEnabled(true);
mMap.getUiSettings().setCompassEnabled(true);
mMap.setOnCameraIdleListener(this); // to track zoom level changes
mMap.setOnMapLongClickListener(this::onMapLongClick);
}
}Auto-scaling reference circle on camera change
Show a grey reference circle whose real-world radius matches the visible area, so users always have a scale reference regardless of zoom level.
MapsActivity.java — onCameraIdle()@Override
public void onCameraIdle() {
int zoom = (int) mMap.getCameraPosition().zoom;
int radius;
if (zoom >= 19) radius = 20;
else if (zoom == 17) radius = 100;
else if (zoom == 15) radius = 300;
else if (zoom == 13) radius = 1000;
else radius = 5000; // and so on …
LatLng centre = mMap.getCameraPosition().target;
if (referenceCircle != null) referenceCircle.remove();
referenceCircle = mMap.addCircle(new CircleOptions()
.center(centre)
.radius(radius)
.strokeColor(Color.GRAY)
.fillColor(Color.TRANSPARENT));
}5. Getting the Device Location
Use the Fused Location Provider for foreground location — it automatically picks the best available source (GPS, Wi-Fi, cell). Use a raw LocationManager inside the foreground service for background tracking.
Fused Location Provider — last known location
MapsActivity.javaprivate FusedLocationProviderClient fusedLocationClient;
// In onCreate:
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
// Fetch last known fix (fast, uses cached value):
private void fetchLastLocation() {
fusedLocationClient.getLastLocation()
.addOnSuccessListener(location -> {
if (location != null) {
currentLocation = location;
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
new LatLng(location.getLatitude(),
location.getLongitude()), 15));
}
});
}One-shot fresh fix on startup
getLastLocation() can return null or a stale fix. Use getCurrentLocation() first; fall back to a one-shot LocationRequest if it times out.
CancellationTokenSource tokenSource = new CancellationTokenSource();
fusedLocationClient
.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, tokenSource.getToken())
.addOnSuccessListener(location -> {
if (location != null) {
currentLocation = location;
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
new LatLng(location.getLatitude(), location.getLongitude()), 16));
} else {
requestSingleFixAndCenter(); // GPS fallback
}
});
private void requestSingleFixAndCenter() {
LocationRequest oneShotRequest = new LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY, 1000)
.setMaxUpdates(1)
.setWaitForAccurateLocation(true)
.setDurationMillis(15_000) // give up after 15 s
.build();
fusedLocationClient.requestLocationUpdates(oneShotRequest,
singleFixCallback, Looper.getMainLooper());
}6. Setting a Destination and Drawing a Geofence
Long-press to drop a pin
MapsActivity.javamMap.setOnMapLongClickListener(latLng -> {
// Convert LatLng to a Location object for distanceTo() later
targetLocation = new Location(LocationManager.GPS_PROVIDER);
targetLocation.setLatitude(latLng.latitude);
targetLocation.setLongitude(latLng.longitude);
// Drop a marker
if (markerTarget != null) markerTarget.remove();
markerTarget = mMap.addMarker(new MarkerOptions()
.position(latLng)
.title("Destination")
.icon(BitmapDescriptorFactory.defaultMarker(
BitmapDescriptorFactory.HUE_ROSE)));
drawAlarmCircle();
appState = STATE_READY;
updateButtons();
});Draw the alarm radius circle
MapsActivity.javaprivate Circle alarmCircle;
private int alarmRadiusMeters = 200; // default 200 m
private void drawAlarmCircle() {
if (targetLocation == null) return;
if (alarmCircle != null) alarmCircle.remove();
alarmCircle = mMap.addCircle(new CircleOptions()
.center(new LatLng(targetLocation.getLatitude(),
targetLocation.getLongitude()))
.radius(alarmRadiusMeters)
.strokeColor(Color.RED)
.fillColor(Color.TRANSPARENT));
}
// Range + / Range - buttons adjust alarmRadiusMeters by 50 m each tap
public void onRangeUp(View v) { alarmRadiusMeters += 50; drawAlarmCircle(); }
public void onRangeDown(View v) { if (alarmRadiusMeters > 100) alarmRadiusMeters -= 50; drawAlarmCircle(); }Polylines — visualise start and current distance
MapsActivity.java// Dark-blue line: fixed line from start point to destination
private void drawOriginLine() {
if (startLine != null) startLine.remove();
startLine = mMap.addPolyline(new PolylineOptions()
.add(new LatLng(originLocation.getLatitude(), originLocation.getLongitude()),
new LatLng(targetLocation.getLatitude(), targetLocation.getLongitude()))
.color(ContextCompat.getColor(this, R.color.colorPrimaryDark)));
}
// Teal line: live line that updates every location fix
private void drawCurrentLine() {
if (currentLine != null) currentLine.remove();
currentLine = mMap.addPolyline(new PolylineOptions()
.add(new LatLng(targetLocation.getLatitude(), targetLocation.getLongitude()),
new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()))
.color(ContextCompat.getColor(this, R.color.colorAccent)));
}7. Background Tracking with a Foreground Service
When the app is minimised or the screen turns off, the Activity's location callbacks stop. A Foreground Service solves this — Android guarantees it stays alive as long as a visible notification is showing.
foregroundServiceType="location") or a Geofencing API call. The manual service approach gives you 1-second GPS polling, which is far more responsive than the Geofencing API's dwell-time transitions.Service skeleton
TrackLocationService.javapublic class TrackLocationService extends Service {
// Broadcasts the service sends back to the Activity
public static final String ACTION_ALARM_TRIGGERED =
"com.example.locationalarm.ALARM_TRIGGERED";
public static final String ACTION_LOCATION_UPDATED =
"com.example.locationalarm.LOCATION_UPDATED";
private final IBinder binder = new TrackerBinder();
private LocationManager locationManager;
private LocationListener listener;
// Parameters set by the Activity via onStartCommand Intent extras
private Location targetLocation;
private int alarmMode = 0; // 0 = arrival, 1 = leaving
private int rangeMeters = 200;
private boolean monitoring = false;
private boolean alarmFired = false;
public class TrackerBinder extends Binder {
TrackLocationService getService() { return TrackLocationService.this; }
}
@Override public IBinder onBind(Intent intent) { return binder; }
@Override
public void onCreate() {
super.onCreate();
createNotificationChannels();
startLocationUpdates();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
alarmMode = intent.getIntExtra("ALARM_MODE", 0);
rangeMeters = intent.getIntExtra("RANGE_METERS", 200);
monitoring = intent.getBooleanExtra("MONITORING", false);
targetLocation = intent.getParcelableExtra("TARGET_LOCATION");
alarmFired = false;
}
startForeground(NOTIFICATION_ID_FG, buildForegroundNotification());
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (locationManager != null && listener != null)
locationManager.removeUpdates(listener);
}
}Registering the GPS listener inside the service
TrackLocationService.javaprivate void startLocationUpdates() {
listener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
currentLocation = location;
// Tell the Activity about the new fix
sendBroadcast(new Intent(ACTION_LOCATION_UPDATED));
// Check alarm condition independently of the Activity
if (monitoring && targetLocation != null && !alarmFired)
checkAlarmCondition();
}
@Override public void onProviderEnabled(String p) {}
@Override public void onProviderDisabled(String p) {}
};
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
1000, // min time interval ms
1, // min distance metres
listener);
}Starting the service from the Activity
MapsActivity.javaprivate void startTrackingService() {
Intent intent = new Intent(this, TrackLocationService.class);
intent.putExtra("TARGET_LOCATION", (Parcelable) targetLocation);
intent.putExtra("ALARM_MODE", alarmMode);
intent.putExtra("RANGE_METERS", alarmRadiusMeters);
intent.putExtra("MONITORING", true);
ContextCompat.startForegroundService(this, intent);
}
private void stopTrackingService() {
stopService(new Intent(this, TrackLocationService.class));
}PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
if (!pm.isIgnoringBatteryOptimizations(getPackageName())) {
Intent i = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + getPackageName()));
startActivity(i);
}8. Checking the Alarm Condition and Triggering the Alarm
Android's Location.distanceTo() returns the distance in metres between two Location objects using the Haversine formula. Call it on every GPS fix.
private void checkAlarmCondition() {
float distance = currentLocation.distanceTo(targetLocation);
boolean triggered =
(alarmMode == 0 && distance < rangeMeters) || // Arrival
(alarmMode == 1 && distance > rangeMeters); // Leaving
if (triggered) {
alarmFired = true; // fire only once per session
triggerAlarm();
}
}
private void triggerAlarm() {
// 1. Vibrate
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(
VibrationEffect.createOneShot(3000, VibrationEffect.DEFAULT_AMPLITUDE));
// 2. Play sound
MediaPlayer mp = MediaPlayer.create(this, R.raw.alarm_sound);
if (mp != null) {
mp.setVolume(0.8f, 0.8f);
mp.setOnCompletionListener(MediaPlayer::release);
mp.start();
}
// 3. High-priority notification (see next section)
sendAlarmNotification();
// 4. Tell the Activity so it can update the UI
sendBroadcast(new Intent(ACTION_ALARM_TRIGGERED));
}In the Activity, register a BroadcastReceiver to catch the signal:
private final BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent intent) {
if (ACTION_ALARM_TRIGGERED.equals(intent.getAction())) {
appState = STATE_ALARM;
updateButtons(); // turns Stop button red, hides others
}
}
};
@Override protected void onResume() {
super.onResume();
registerReceiver(alarmReceiver,
new IntentFilter(TrackLocationService.ACTION_ALARM_TRIGGERED));
}
@Override protected void onPause() {
super.onPause();
unregisterReceiver(alarmReceiver);
}9. Posting Notifications
Two notification channels are required: one silent channel for the foreground service (so the persistent icon does not make noise) and one high-importance channel for the alarm.
Create the channels once
TrackLocationService.javaprivate static final String CH_SERVICE = "ch_fg_service";
private static final String CH_ALARM = "ch_alarm";
private void createNotificationChannels() {
NotificationManager nm = getSystemService(NotificationManager.class);
// Silent channel — foreground service heartbeat
NotificationChannel svc = new NotificationChannel(
CH_SERVICE, "Tracking active", NotificationManager.IMPORTANCE_LOW);
nm.createNotificationChannel(svc);
// Loud channel — alarm fires here
NotificationChannel alarm = new NotificationChannel(
CH_ALARM, "Alarm", NotificationManager.IMPORTANCE_HIGH);
alarm.enableVibration(true);
alarm.setLockscreenVisibility(NotificationCompat.VISIBILITY_PUBLIC);
nm.createNotificationChannel(alarm);
}Foreground service notification (silent)
TrackLocationService.javaprivate Notification buildForegroundNotification() {
PendingIntent pi = PendingIntent.getActivity(this, 0,
new Intent(this, MapsActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
String modeText = alarmMode == 0 ? "Arrival" : "Leaving";
return new NotificationCompat.Builder(this, CH_SERVICE)
.setContentTitle("Alarm active — " + modeText + " mode")
.setContentText("Monitoring within " + rangeMeters + " m")
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pi)
.setOngoing(true) // cannot be swiped away
.build();
}Alarm notification (high-priority, lock-screen visible)
TrackLocationService.javaprivate void sendAlarmNotification() {
String title = alarmMode == 0 ? "You have arrived!" : "You have left!";
String text = alarmMode == 0
? "Inside " + rangeMeters + " m of your destination"
: "Outside " + rangeMeters + " m from your location";
PendingIntent pi = PendingIntent.getActivity(this, 0,
new Intent(this, MapsActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Notification n = new NotificationCompat.Builder(this, CH_ALARM)
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pi)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_ALARM)
.setAutoCancel(true)
.build();
getSystemService(NotificationManager.class).notify(888, n);
}10. Managing App State
A single integer variable (appState) drives all UI decisions. Using an explicit state machine prevents UI bugs caused by buttons appearing at the wrong time.
| Constant | Value | Meaning | Visible controls |
|---|---|---|---|
STATE_IDLE | 0 | Waiting for destination to be set | Measure |
STATE_READY | 1 | Destination set, ready to monitor | Start, Arrival/Leaving, Range +/−, Measure |
STATE_TRACING | 2 | Foreground service running, monitoring active | Stop (yellow) |
STATE_ALARM | 3 | Alarm has fired | Stop (red) |
private void updateButtons() {
switch (appState) {
case STATE_IDLE:
btnStart.setVisibility(View.INVISIBLE);
btnMode.setVisibility(View.INVISIBLE);
btnRangeUp.setVisibility(View.INVISIBLE);
btnRangeDown.setVisibility(View.INVISIBLE);
break;
case STATE_READY:
btnStart.setText("Start");
btnStart.setVisibility(View.VISIBLE);
btnStart.setBackgroundColor(Color.GREEN);
btnMode.setVisibility(View.VISIBLE);
btnRangeUp.setVisibility(View.VISIBLE);
btnRangeDown.setVisibility(View.VISIBLE);
break;
case STATE_TRACING:
btnStart.setText("Stop");
btnStart.setBackgroundColor(Color.YELLOW);
btnMode.setVisibility(View.INVISIBLE);
btnRangeUp.setVisibility(View.INVISIBLE);
btnRangeDown.setVisibility(View.INVISIBLE);
break;
case STATE_ALARM:
btnStart.setText("Stop");
btnStart.setBackgroundColor(Color.RED);
break;
}
}
// Start / Stop button handler
public void onStartStopClicked(View v) {
if (appState == STATE_TRACING || appState == STATE_ALARM) {
stopTrackingService();
appState = STATE_IDLE;
} else {
if (targetLocation == null) { showHint("Set destination first"); return; }
startTrackingService();
appState = STATE_TRACING;
}
updateButtons();
}A 2-second Handler loop keeps the info bar updated during tracing without coupling the UI refresh rate to the GPS update rate:
private void startInfoLoop() {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override public void run() {
if (appState == STATE_TRACING && currentLocation != null) {
float dist = currentLocation.distanceTo(targetLocation);
infoBar.setText("Remaining: " + formatDistance((int) dist));
}
handler.postDelayed(this, 2000); // repeat every 2 s
}
});
}11. Tips, Gotchas & Best Practices
| Topic | Advice |
|---|---|
| One alarm per session | Use a boolean flag (alarmFired) in the service and set it to true on first trigger. Reset it only when the user explicitly presses Stop. This prevents repeated alarms from GPS jitter near the boundary. |
| Screen rotation | Save targetLocation, appState, alarmMode, and alarmRadiusMeters in onSaveInstanceState() and restore them in onCreate(). Re-draw all map overlays in onMapReady(). |
| GPS jitter at rest | GPS accuracy at rest is typically ±3–15 m. For an arrival alarm keep the radius ≥ 100 m to avoid false triggers. For leaving alarms consider adding a short dwell-time check (e.g., must be outside radius for 3 consecutive fixes). |
| Background service lifetime | Request battery optimisation exemption. Instruct users to whitelist the app in device-specific battery settings (Samsung: Sleeping apps → Never; MIUI: Autostart + Battery saver). |
| API key security | Restrict your Maps API key to your app's SHA-1 signing certificate and package name. Never hardcode a key in a shared source file; use Android Secrets Gradle Plugin or local.properties. |
| Google Play policy | ACCESS_BACKGROUND_LOCATION requires a prominent in-app disclosure dialog before the system permission prompt. The disclosure must describe the specific feature using background location, not just a generic statement. |
| Testing without travelling | Use the Android Emulator's GPS controls (Extended Controls → Location → Routes) to simulate movement. Create a GPX route from your current position to a point 300 m away to test the arrival alarm indoors. |
| Localisation | All UI strings should go in res/values/strings.xml. Provide translations in res/values-XX/strings.xml for each locale. The system picks the right file automatically based on the device language. |