Android · Kotlin · MIT

Raise your phone.
The call answers itself.

Pick the phone up, bring it to your ear, and the call connects. No swipe, no tap. What makes it trustworthy is not that it answers — it is everything it refuses to answer.

Download APK ↓ Read the source
Android 13+ No network access Root not required 38 unit tests
Accelerometer · one raise-to-ear gesture m/s² deviation from 1g
PICKUP ≥ 2.0 SETTLE ≤ 1.5 RING · SENSORS ON PICKUP PROXIMITY NEAR ANSWERED
Ring → answer
1.4 s
Gates passed
6 / 6
Sensor samples
185
Sensors after
OFF
/01 — THE PROBLEM

Proximity alone is not a gesture.

Apps in this space have existed for years. Nearly all of them trigger on one thing: the proximity sensor reporting NEAR. That is also exactly what a pocket reports, which is why their reviews are full of people whose phone answered a call from inside their jeans.

The sensor cannot tell an ear from a trouser pocket — both are simply something close to the screen. Treating that as consent is the bug. Here, proximity is never the trigger. It is the last of three checks, and the weakest one.

/02 — THE MECHANISM

Three signals, three questions.

Each answers something the others cannot. All three must agree.

Motion

Did you mean it?

A deliberate lift produces a burst of acceleration well past 2.0 m/s². A phone that has not moved cannot have been picked up, so nothing else is even evaluated.

Proximity

Is it against something?

Confirmation only — and it is rejected outright unless the sensor was seen FAR after the call started ringing. A phone covered since before the call can never answer. This one rule kills most pocket cases.

Stillness

An ear, or a pocket?

A phone at your ear has stopped. A phone in a pocket on a moving body never does. Measured over a short 300 ms window, because the 800 ms window used for pickup still contains the lift that just happened.

Timing trap

The window must close first

Validation runs 500 ms after the phone reaches your ear. Measuring stillness over 800 ms would always include the lift — making the gate unpassable and every real gesture fail. Two windows, not one.

Battery

Sensors off by default

Nothing samples until a call actually rings, and everything unregisters the moment the decision is made. Between calls the app is a suspended callback — no polling, no wakelocks, no timers.

Testability

A state machine with no clock

The decision logic is pure Kotlin — no Android imports, time arrives on events, effects leave through a callback. All 38 tests run on the JVM. No emulator, no real phone calls.

/03 — THE GUARDS

What it refuses to do.

Every rejection is logged with the measured value, so when it declines you can see precisely which gate stopped it.

RefusedPocket · walking
Proximity → NEAR
NEAR rejected — sensor covered
since before the call
RefusedPushed back into pocket
Gesture validation…
Phone is still moving
(energy 4.0, want ≤ 1.5)
RefusedTable · hand over sensor
Proximity → NEAR
NEAR rejected — no recent pickup
RefusedFace-down on a desk
Gesture validation…
Phone is face-down
(tilt 171.4deg)
AnsweredPicked up from a desk and raised to the ear
Call state → RINGING          sensors registered
Motion detected — pickup      energy 6.1 ≥ 2.0
Proximity → NEAR              FAR seen earlier this ring ✓
NEAR debounce passed          stable for 500 ms
Gesture validation passed     tilt 84° · settled 0.3 · earpiece up
Answer successful             sensors unregistered
/04 — PERMISSIONS

Six. And no internet.

There is no network code anywhere in the project. No phone number or contact name is ever read or written to a log.

PermissionWhy
READ_PHONE_STATEKnow when a call is ringing
ANSWER_PHONE_CALLSAnswer it — an ordinary runtime permission since Android 8
FOREGROUND_SERVICE
+ _SPECIAL_USE
Stay alive to notice the ring
POST_NOTIFICATIONSThe ongoing notification Android requires of any such service
RECEIVE_BOOT_COMPLETEDKeep working after a reboot

Root is not required. The root path is an unused fallback — the public telecom API does the work on a stock, unrooted phone.

/05 — A HONEST LIMITATION

WhatsApp calls are not supported.

It was built, it was tested on a real device, and it was removed. A VoIP app's answer button is an activity PendingIntent — pressing it makes that app open its call screen, and Android judges that launch against our background privilege, not theirs.

The gesture worked perfectly every time. The press was simply discarded.

Pixel 9 · Android 17 · app backgrounded · logcat
E ActivityTaskManager: Background activity launch blocked!
  intent: com.whatsapp/.calling.ui.VoipActivityV2 (ACCEPT_CALL)
  realCallingPackage:   com.example.earautoanswer
  realCallerStartMode:  MODE_BACKGROUND_ACTIVITY_START_ALLOWED  ← we allowed it
  balAllowedByPiSender: BSP.ALLOW_BAL                           ← our side agreed
  resultIfPiSenderAllowsBal: BAL_BLOCK                       ← blocked anyway
  realCallingUidHasVisibleActivity: false
E ActivityTaskManager: Abort background activity starts

It works only while the app is open on screen, which is useless for answering calls. Rather than ship something that fails silently, the feature was deleted — and the finding documented so nobody rebuilds it.

/06 — GET IT

Install, or build it yourself.

Install

Download the APK

Sideloaded, so Android will warn about an unknown developer. Requires Android 13+, a proximity sensor and an accelerometer.

Latest release ↓
Build

Clone and assemble

Any JDK 17+. No signing key needed for a debug build.

git clone https://github.com/ahkamboh/ear-auto-answer
cd ear-auto-answer
./gradlew :app:assembleDebug
Calibration

Tune it to your handset before trusting it

Thresholds are set for a Pixel-class phone. Open Test Sensors, raise the phone to your ear, and watch settleEnergy — it must fall into the green band within about half a second. If pickup and rest overlap, adjust pickupMovementThreshold and maxSettleMovementEnergy in AutoAnswerConfig.kt. Every tunable value lives in that one file.