On 28–29 May 2026, I attended this NUS course Engineering Visual Perception for Autonomous Systems (by Tian Jing), which covers from sensors to deployment. It explains how vision sensors (cameras, LiDAR, radar) capture data and how images are represented as pixel arrays with color channels.
The following is listed for reference — not that I fully understood every detail, but it captures the key topics covered.
Image Classification
The core classification task assigns a label to an entire image. Two architectures are taught:
Closed-set → Open-world: VLMs with text prompts for zero-shot detection
Accuracy → Robustness: adversarial attacks and security
Perception → Reasoning: spatial reasoning, conversational queries over visual data
Hands-on Workshops
Workshops were run on Google Colab using provided Jupyter notebooks in Python: solar panel defect classification (ResNet/ViT) and candy detection (YOLOv11).
Part II
Reflections: AI, Developers & the Changing Landscape
The "Black Box Productivity" Paradox
The workshop experience perfectly illustrates the current shift: we ran ResNet and ViT notebooks on Colab, got correct results, without understanding the source code. This works — until it doesn't. When the model underperforms on your own data, debugging requires understanding the layers you skipped over.
The Freemium Trap & AI Investment Cycle
Massive capital is flowing into AI infrastructure (OpenAI, Google, Microsoft). The playbook is clear: offer free access to build dependency, then monetize. Google Colab already tiered its GPU access; GitHub Copilot is capping free usage; OpenAI raised API prices. These companies are subsidizing adoption with investor money, knowing that once teams build workflows around these tools, switching costs are high.
The risk for developers and organizations: building critical skills and pipelines on platforms whose pricing you don't control.
Developer Role Shift: From Creator to Verifier
AI can now generate boilerplate, debug simple errors, and scaffold entire projects. This shifts the developer's core job:
Old Role
New Role
Write code from scratch
Verify AI-generated code for correctness, security, edge cases
Memorize syntax/APIs
Evaluate architectural trade-offs
Debug line-by-line
Validate system-level behavior
Individual contributor
AI orchestrator + quality gate
The challenge: verification requires deeper understanding than writing. You can copy-paste a ResNet training loop from Copilot, but catching a subtle data leakage between train/validation splits requires real expertise. The skill floor rises, not falls.
Fresh vs. Experienced Developers — Corporate Perspective
Factor
Fresh Developer
Experienced Staff
AI tool adoption
Fast, native users
Slower adoption, but deeper judgment
Code verification
Limited — can't spot what they haven't seen
Strong — pattern recognition from years of debugging
Cost
Lower salary
Higher salary
Risk with AI-generated code
High — may accept flawed output ("it runs, ship it")
Lower — skepticism from experience
Domain knowledge
Minimal
Deep, hard to replace
Adaptability
High
Varies
The honest answer: neither alone is optimal. And this mirrors what happens in the classroom too — in a class like the EVPAS workshop, younger students may grasp new concepts like "occlusion" or "mAP" faster, partly because they're closer to academic training and often operating in English natively. But speed of recall is not depth of understanding.
You may forget the word "occlusion" by the next day's exam, yet still arrive at the correct answer by reasoning through elimination — recognizing that partial visibility behind another object is the only option that fits. That's exactly the kind of structured problem-solving that experienced professionals bring.
AI-generated code works the same way — the person who can logically evaluate whether an output makes sense beats the person who memorized the syntax but can't reason about edge cases.
Fresh developers are productive fast with AI tools but dangerous without mentorship — they become the person in the workshop who "gets results but doesn't know the details." In production, that gap causes incidents.
Experienced developers bring the judgment to verify, debug, and architect — but if they resist AI tools, they lose the speed multiplier. Being a non-native English speaker or an older learner in a fast-moving field adds friction, but it also forces a habit of deliberate learning — double-checking, cross-referencing, reasoning by elimination — which is precisely the verification mindset the AI era demands.
The most effective corporate strategy: pair experienced staff (as verifiers/architects) with AI-augmented fresh developers (as accelerated producers). The experienced developer's role shifts from writing code to reviewing AI-assisted output and teaching juniors what to watch for.
The Real Challenge
The deeper issue is asymmetric skill decay: if junior developers never learn to build from scratch (because AI does it), they never develop the judgment needed to verify. You end up with a generation that can prompt but can't debug — exactly like running a Colab notebook successfully without understanding why it works. The moment the pre-built solution breaks, they're stuck.
The developers who will thrive are those who use AI to move faster while insisting on understanding what the code does — treating AI as a power tool, not a replacement for knowledge. And ironically, the "disadvantage" of being an older, non-native learner — having to slow down, reason carefully, and verify before committing — may be the exact discipline this new era rewards most.
Conclusion
In this fast-pacing AI era, learning is truly endless — new models, new frameworks, new paradigms emerge before you've finished digesting the last one. It can feel impossible to catch up, and the honest truth is: you probably can't catch everything. But that's not the point. As the Chinese saying goes, 姜是老的辣 — "older ginger is spicier" — meaning experience carries a sharpness that youth alone cannot replicate. The value of a seasoned professional is not in memorizing every new API or chasing every trending tool, but in knowing what matters: staying aware of ongoing trends, keeping a watchful eye on where the technology is heading, and — most critically — blending that awareness with the deep domain knowledge accumulated over more than 20 years of real-world practice. Frameworks will come and go, AI tools will rise and be replaced, but the understanding of how systems actually work in production, why certain designs fail under pressure, and what questions to ask when something looks too good to be true — that kind of wisdom is earned, not prompted. The path forward is not to outrun the young, but to out-think the noise: learn selectively, verify rigorously, and let experience be the compass that AI cannot replace.
A question for you, the reader: Every one of us is getting older — that clock doesn't pause for any technology wave. So have you thought about your own future in this AI era? With AI closing the gap between one person and an entire team, are you going to run a one-person company powered by AI — leveraging your domain expertise with AI as your tireless co-worker? Or will you continue seeking traditional employment, hoping the next round of layoffs passes you by? There's no wrong answer, but there is a wrong strategy: not thinking about it at all.
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.
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 + notification
2. 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.
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:
Never commit your API key to a public repository. Add google_maps_api.xml to .gitignore, or use Android Secrets Gradle Plugin to load it from a local local.properties file.
Location and notification permissions must be requested at runtime. Do it in two stages:
1
Fine location — request when the Activity starts.MapsActivity.java
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);
}
2
Background location (Android 10+) — Google Play policy requires you to show an in-app disclosure before requesting this. Show an AlertDialog that explains exactly why background location is needed, then request it only if the user agrees.MapsActivity.java
private 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();
}
3
Notification permission (Android 13+) — handled in onCreate.MapsActivity.java
private 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);
}
}
}
Tip: Handle 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.
public 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.java
private 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.
MapsActivity.java
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.java
mMap.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.java
private 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.
A foreground service is not optional for this use case. Background location requires either a foreground service (with 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.java
public 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.java
private 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);
}
Battery optimisation: On many devices (especially Samsung and Xiaomi) aggressive battery management kills foreground services. Request the exemption at startup:
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.
TrackLocationService.java
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:
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.java
private 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.java
private 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();
}
private 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.
A 2-second Handler loop keeps the info bar updated during tracing without coupling the UI refresh rate to the GPS update rate:
MapsActivity.java
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.