add automatic call recording + rework call recording for better testing - #58
Open
inthewaves wants to merge 34 commits into
Open
add automatic call recording + rework call recording for better testing#58inthewaves wants to merge 34 commits into
inthewaves wants to merge 34 commits into
Conversation
inthewaves
force-pushed
the
autocallrec
branch
2 times, most recently
from
May 20, 2026 02:09
2619039 to
fb46285
Compare
|
Great to see this moving forward again - thanks! Just a small suggestion to consider for more flexibility - add the option to also record calls of almost all contacts by default, except a few selected contacts. Basically, separate record yes/no switches for unknown callers / non-contacts / contacts and then possible override for each contact in either direction (yes/default/no). Use case: most of the contacts are local customers of my business whose calls should be recorded (same jurisdiction as mine, recording is legal), except a few that shouldn't (trusted people for whom it's not necessary, or the rare international customer from a different jurisdiction where call recording is illegal). |
toString as well to be sure.
… times Needs to follow framework permission states, although not sure why anyone would attempt to disable microphone permissions for the active Dialer app...
Move call recording preferences from MODE_MULTI_PROCESS SharedPreferences to MultiProcessDataStore backed by a proto schema in credential encrypted storage. Update the existing Java settings fragment and recorder services to read and write through the DataStore bridge. Android Developers' SharedPreferences reference recommends against using SharedPreferences for new storage and notes that the class does not support use across multiple processes: https://developer.android.com/reference/android/content/SharedPreferences MODE_MULTI_PROCESS is deprecated and cannot serialize concurrent updates across the main Dialer process and the call recording service process. DataStore gives the preference state an explicit schema, typed output format enums, serialized writes, and read after write consistency. Migrate existing call recording SharedPreferences into DataStore the first time credential encrypted storage is available, then remove the migrated legacy keys. When the user is not unlocked, expose default disabled values and ignore writes instead of touching credential encrypted storage. Keep automatic recording fields in the same call recording schema so later settings share one store and default to disabled until they are wired up. This also introduces Kotlin sources for Dialer. DataStore is Kotlin and coroutine native, while existing Java settings and incallui paths continue to use Future wrappers and blocking service bridges at the boundary. Development note: MultiProcessDataStore uses AndroidX DataStore's SharedCounter JNI library. Dialer enables embedded native libraries so Soong packages libdatastore_shared_counter.so uncompressed in the APK and derives the final extractNativeLibs=false manifest value. This changes the Dialer APK contents, so when testing with incremental builds, a new build fingerprint is needed to get the package manager to read the new JNI library.
Add the DialerTests instrumentation target and a debuggable DialerForTesting helper app so call recording tests can run against the real package without optimized code. Test: atest DialerTests
…numbers Add worker thread number canonicalization and selected number contact lookup helpers so automatic recording settings, cleanup, and policy code can compare stored numbers against local contacts. Preserve ContactInfoCache normalized numbers for local contact results. When the contact row lacks a normalized number, fill it from the caller info worker callback so later selected number policy can use the cache without doing fallback parsing on the main thread. Test: atest DialerTests
Convert the existing call recording settings fragment to Kotlin while keeping the same four recorder preferences and DataStore-backed update flow. This leaves the automatic recording settings UI for a later commit. Test: atest DialerTests
Add the shared warning acknowledgement helper, settings enable flow, and pure automatic recording eligibility checks used by settings and later incallui wiring. We don't allow user to turn on automatic recording until they've opted into the same consent. Test: atest DialerTests
Add the automatic recording section to the main call recording settings screen, plus selected number management for the selected contact policy. Store canonical numbers, resolve them for display, and reuse the shared warning and permission flow before enabling selected number recording. Filter automatic recording policy keys from backup because they include selected numbers and user consent state. Test: atest DialerTests
Add a cleanup job for selected automatic recording numbers that no longer resolve to local contacts. The scheduler observes DataStore preferences and only keeps the job scheduled while selected number recording has stored numbers to validate. Test: atest DialerTests
Add the Kotlin interfaces and default adapters that let automatic recording policy read the active call, contact info, loaded call recording preferences, and permission state without binding the policy to process singletons. Add AutoRecordingDecider to turn those inputs into a per-call automatic recording choice. This keeps contact lookup and number matching separate from the later incallui wiring. Test: atest DialerTests
Wire the decider into incallui through CallRecordingCoordinator and provide production dependencies through DialerRootComponent. Eligible active calls load DataStore preferences for the decision, then arm recording and start once the recorder service is connected. The dependency bundle keeps Android process state outside policy tests while production uses the DialerRootComponent binding. At this point CallRecorder still owns the call list entry point, the coordinator owns automatic recording decisions, and the manual record button still uses its existing CallButtonPresenter flow. Test: atest DialerTests
The recording progress task kept reposting after getActiveRecording() failed against a dead recorder service binder. That could spam logcat and keep progress callbacks alive after the call ended. Stop progress polling when the recorder service disconnects or throws, and finish the active recording before tearing down the last live call. Test: atest DialerTests
Both recorder services should use their normal stop path when the service is destroyed. This finalizes active recordings through the MediaStore completion update instead of only releasing recorder resources. Test: atest DialerTests
Keep active recording state in CallRecorder instead of querying the recorder service over AIDL for UI state. This avoids stale service metadata and removes main thread binder queries from progress listener replay. Clear client state when recording stops, when the service disconnects, and when the bound service throws. Keep service implementations focused on start and stop recording calls. Also use copy-on-write (🐮) listener set so mutation can be done while iteration is in progress. Test: atest DialerTests
Show a recording indicator in the contact grid bottom row while call recording is active and the call timer is visible. Update the contact grid from the same recording state and duration callbacks that update the record button, so late UI refreshes keep the timer row in sync. Test: atest DialerTests
Show a transient incallui message when recording starts automatically. The message is delivered from CallButtonPresenter, waits for the button grid if needed, fades out after a short timeout, and can be dismissed by tapping it. Test: atest DialerTests
Show call recording state in CallStyle notifications. Active recordings display recording text, and incoming calls show whether automatic recording will start or which permission is missing. Load call recording preferences asynchronously for notification rendering and update the notification after the load completes. CallStyle ignores Builder#setSubText(), so this uses verification text as the visible secondary slot. Test: atest DialerTests
Show a record switch on the audio incoming call screen when manual recording can be chosen. Keep it hidden for video upgrades, reflect automatic selected contact decisions after contact lookup, and pass the user's choice to CallRecorder so recording starts after answer. Test: atest DialerTests
CallRecorderServiceV2 can accept a start request before the worker backed recorder reports that audio capture is running. Track the accepted recording session immediately so service cleanup and failure handling have stable metadata for the in progress MediaStore row. Add a narrow RecordingBackend interface so service lifecycle tests can cover startup, failure, and cleanup without depending on real audio I/O. Test: atest DialerTests
Replace the separate active and armed booleans in CallRecorder with one RecordingState helper that tracks the active call id, armed call id, and whether a start event should be replayed to late listeners. This prepares the recorder service client for later manual recording flow without adding warning or permission UI to this commit. Test: atest DialerTests
Add the manual recording flow to the coordinator for warning acknowledgement, RECORD_AUDIO permission, and starting or arming the current call. Keep the existing call button entry point until the call button UI is wired to the coordinator. The preceding commit already made CallRecorder track armed recording state, so this change only adds the manual policy flow and its dependencies. Test: atest DialerTests
Move recorder service bind and unbind bookkeeping behind CallRecorderServiceBinding so tests can use a fake service connection instead of mutating CallRecorder's service and bound state directly. Production still uses Context.bindService with the same service selection path. Test: atest DialerTests
Show the record button while an outgoing call is dialing or connecting. A press arms the current call through CallRecordingCoordinator and starts recording only after it becomes active, so setup time is not counted. Update the call button UI to show armed recording as enabled without starting the recording timer, and forward permission results back to the pending manual flow. Test: atest DialerTests
Since conference calls inherently add new people to the call, explicitly stop recording when making a conference call, and do not start automatic call recordings from a conference call. It's worth noting that Google Dialer specifically disallows any call recording of conference calls. Treat conference calls and conference children as the call list state that requires an explicit record press before recording can continue. Stop the current recording when a recorded call joins a conference, and prevent automatic recording from silently restarting after the conference state clears. Keep ordinary active and held calls recordable: if the active call matches unknown caller or selected contact policy, automatic recording can start for that active call while the previous call is held. The tests cover some of the expected behavior. Note that if you are added to a conference, it's not always possible for the device to tell if you were added to a conference or not (other than you hearing different voices). e.g., the carrier config https://github.com/GrapheneOS/platform_frameworks_base/blob/2026050900/telephony/java/android/telephony/CarrierConfigManager.java#L1540 seems to imply that this is not mandatory. With local testing on my numbers, this is indeed the case: conference peers weren't able to tell they were added to a call. So it's still possible for call recording to continue when you're added to a conference, just not when you're the one making the conference/merge. Again, it's up to the user to follow laws on this (multi-party consent, etc.) Test: atest DialerTests
Call recording settings are stored in DataStore under credential encrypted storage, so manual recording should not start before the user unlocks the device. The recorder services are not direct boot aware, and the output path is created through MediaStore under Recordings/CallRecordings on external storage. Keep the record button available, but show a toast and leave the recording flow idle until the user unlocks. Test: atest DialerTests
Keep CallRecorder focused on the bound recording service and active recording state. Add CallRecordingController to own CallList events, coordinator lifetime, and UI facing call recording actions. This keeps the service client from reaching back into CallList and Dagger policy dependencies, while preserving the existing coordinator as the owner of manual and automatic recording decisions. Test: atest DialerTests
Telecom can briefly report every call as held while swapping between calls. CallList.hasLiveCall() treats that as no live call, which made call recording clear automatic choices and user stopped recording state during the swap. A later active update could then automatically record a call the user had already stopped. Track whether any ongoing call still exists instead of only live calls. Update the tests so the all held transition is modeled explicitly. Test: atest DialerTests
These tests fake calls like what CTS tests do so that the actual Dialer UI and Telecom integration can be tested. Implementing some of these as unit tests would require the unit tests to mirror Telecom behavior which isn't good. Use a separate helper connection service APK so Telecom calls are not tied to DialerForTesting's process. Currently we don't validate the actual call recording data, just that the file exists. Also, the permissions tests would need a bit more work. Test: atest DialerIntegrationTests
If Dialer crashes or is stopped during a call, a user choice to stop automatic recording for that call could be lost, causing recording to start again when incallui returns. Persist the per-call choice temporarily on disk so a process restart preserves the recording decision. DialerCall ids are process local, so incallui process death can recreate the same live Telecom call with a new id. Persist a transient credential encrypted DataStore latch keyed by Telecom call creation time before honoring user choices that stop automatic recording. The latch prevents a restarted incallui process from starting automatic recording again for a call the user already stopped or declined on the answer screen. It is separate from user settings: the coordinator prunes it against the live call list and clears it when the call session ends. It does not migrate or back up like durable call recording preferences. Manual starts and incoming call switch choices clear or set the latch before the coordinator updates in-memory state. Test: atest DialerTests DialerIntegrationTests
Add a Dialer test target that runs from a separate instrumentation APK instead of instrumenting Dialer, so tests can stop or crash Dialer while Telecom keeps a live call alive through the helper connection service. Use test-only target receivers in DialerForTesting for preference seeding, session state cleanup, conference merge, and process crashes. Cover automatic recording state across process kill, force stop, com.android.dialer crash, and incallui crash. Test: atest DialerOutOfProcessTests
Add a callback path for recorder failures that happen after startRecording returns. Both recorder services notify incallui when the active recording fails, and CallRecorder clears local recording state, stops progress callbacks, and shows the failure toast. For V1, listen to MediaRecorder errors and delete the incomplete MediaStore row. For V2, let the backend report writer failures to the service so cleanup can run off the binder call path. Dead callbacks are logged and ignored because the client process may have gone away. Test: atest DialerTests
The old stop path synchronously waited for recorder service teardown from the incallui request path. That could block the caller, including same process calls and V2 writer cleanup. Request recorder stop over AIDL and report completion through the recorder callback. Keep the service busy until cleanup finishes. Prevent incallui from starting manual or automatic recording again while that stop is pending. Test: atest DialerOutOfProcessTests DialerIntegrationTests DialerTests
Treat recorder backend failures after start as recording errors rather than successful stops. If a backend reports an active failure or throws while finishing, clear the active service session, close the backend, delete the incomplete pending MediaStore row, and notify incallui through the recording error callback. Surface that runtime failure to the user with the existing toast path, a transient incallui message, and a new high importance call recording errors notification channel. Each failure uses a fresh notification id so the alert is noisy again, and the content intent is immutable. Do not expose partial recordings as saved files until there is an explicit recovery path that can label and validate them. This avoids turning a recorder failure into a false success for files that may be truncated, corrupt, or missing required container finalization. Expand unit and integration coverage for both failure shapes: asynchronous backend error callbacks after recording has started, and failures during stop that can happen while finishing the file. The integration coverage also verifies that the user warning surfaces on the call recording error channel, the partial recording row is removed, and incallui leaves recording off. Use a compile time DialerForTesting gate for the test backend replacement. Production builds keep the disabled constant, while the test target supplies the enabled source needed for the failure path tests. Test: atest DialerTests DialerIntegrationTests
… record Keep the incoming answer switch hidden while preferences and contact lookup decide whether automatic recording applies. Show it checked on only when the final lookup says the call will be recorded automatically, giving the user an opt out before answering. We hide the switch in all other cases, i.e. the switch only shows when automatic call recording would be on for that call. We could add an option to always show this switch though, but currently it seems like it's better to just tell users to set auto record settings appropriately. Do not write a disabled per call choice for hidden switch states. Calls outside this opt out UI should flow through the shared automatic recording policy after answer. Cover selected contact, non-contact, pending lookup, hidden no-rule, and visible checked UI states so the answer screen contract is tested at unit and integration levels. Test: atest DialerTests DialerIntegrationTests DialerOutOfProcessTests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes GrapheneOS/os-issue-tracker#4740
Closes GrapheneOS/os-issue-tracker#7703
Closes GrapheneOS/os-issue-tracker#5243
Closes GrapheneOS/os-issue-tracker#4307
This adds user controlled automatic call recording to Dialer while keeping manual recording available from incallui. Users can configure automatic recording for non-contacts/unknown callers and for explicitly selected contact numbers. Each decision is made per call after the existing recording warning, permission, call state, user unlock, and contact lookup gates have passed.
Like Google Dialer, automatic recording is not a blanket "record every call" switch. It is limited to selected numbers or the non-contacts/unknown caller class, and conference calls are treated as a separate consent boundary. Manual recording remains available when the user explicitly presses record.
Notification for ongoing call:
After answering or picking a call that would get auto recorded: Temporary in-app message
Incoming call for a call that would be auto recorded (switch not shown if call wouldn't be auto recorded, so it acts as a per-call opt out switch)
Making an outgoing call: Record button can be turned on to arm a recording; recording will start after call connects if the call recording is in the armed state. This is automatically displayed as on when the outgoing call would be auto recorded, so user can opt out of auto recording for this specific call if they wanted to
User visible behavior
The settings UI gains automatic recording controls for non-contacts/unknown callers and selected
numbers, including selected number management and stale contact cleanup. incallui reflects the
recording lifecycle with an armed/recording record button state, a recording dot next to the call
timer, notification text, and a short in-app message when automatic recording starts.
Incoming audio calls can show a record switch before answering if auto recording is on for that call as an early opt-out switch. Missing microphone permission disables the switch with a permission notice; missing contacts permission does not block the user from choosing recording for that incoming call. I don't know why someone would ever disable these permissions if they have com.android.dialer as the default Dialer app, but we should handle that case too.
Outgoing calls can be armed while dialing or connecting. Recording starts only once the call becomes
recordable, so setup time is not counted as recording time. Recording is blocked before user unlock
because the preferences, recorder service, and media path are not direct boot aware and depend on
credential encrypted storage.
Architecture
The main split is:
CallRecorder: recorder service binding, local active/armed recording state, and UI listenerreplay.
CallRecordingController: the incallui singleton that owns call list events and presents a Java-friendly surface to presenters and notification code.CallRecordingCoordinator: manual and automatic recording policy, including user stop choices,conference rules, process restart choices, and direct boot checks.
This keeps recorder service state, call list events, and recording policy in separate units, so
unit tests can exercise each piece without driving the whole incallui stack.
Storage moved from SharedPreferences in the deprecated multiprocess mode to proper multiprocess Jetpack DataStore. Kotlin policy and DataStore code use suspend APIs directly. Remaining Java callback/Future bridges are kept at Java boundaries such as incallui presenters, service binding, and test command entry points.
The PR also adds selected number canonicalization, contact lookup helpers, stale selected number
cleanup, and a narrow service binding interface so lifecycle behavior can be tested without mutating
CallRecorderinternals.The CallRecordingService itself was also made more async so that we stop doing operations on main thread (e.g. in the old design, stopping recording would block on main/UI thread on media finalization) and now also reports errors during recordings (e.g. useful when running out of space)
Conference policy
Conference calls change the participant set after the original recording decision. I'm not a lawyer, but this might have implications, e.g. adding another person to the call who resides in a different jurisdiction with different laws. To avoid silently recording newly joined people:
action explicit
Separate active/held calls are handled differently from conferences. A second ringing call does not
stop the current recording. When the user answers a second call and the recorded call goes on hold,
the old recording stops; if the new active call is independently eligible, it can start its own
automatic recording unless the user has stopped automatic recording for that session.
Google Dialer appears stricter for conferences: its availability path rejects conference calls,
children of conference calls, and multiple calls in progress, and its automatic activation path also
treats conference calls as ineligible. This PR keeps manual recording available after an explicit
user press.
There is also a platform limitation: if this device is added to a conference by someone else, the
local device may not be able to tell that the call became a conference. For example, the carrier config key
KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_ON_PEER_BOOLdocuments that conference event packagesupport for peers is carrier dependent. Local testing showed peers could not always tell they were
added to a conference, so the explicit conference handling mainly covers conferences created or
merged on this device.
Of course, users are always expected to follow laws on automatic call recording
Tests
This PR also includes brand-new 3 test suites for Dialer. Currently the tests are focused on call recording:
DialerTestscover storage, policy, presenter, service, and lifecycle behaviorDialerIntegrationTestscover contact and Telecom based call flows.DialerOutOfProcessTestscover process death/restart behavior, including force stop,com.android.dialercrash, incallui crash, private callers, non-contact outgoing calls, disabled automatic recording, active automatic recording, and conference cases. This tests things like if someone turns off recording for a call that was initially automatically recorded, it should not silently start recording again if Dialer crashes or stops for any reason.