diff --git a/java/com/android/incallui/call/CallRecorder.java b/java/com/android/incallui/call/CallRecorder.java index 8509b5fe3..b85595b2d 100644 --- a/java/com/android/incallui/call/CallRecorder.java +++ b/java/com/android/incallui/call/CallRecorder.java @@ -36,7 +36,6 @@ import com.android.dialer.callrecord.ICallRecorderService; import com.android.dialer.callrecord.impl.CallRecorderService; import com.android.dialer.callrecord.impl.CallRecorderServiceV2; import com.android.dialer.location.GeoUtil; -import com.android.incallui.call.state.DialerCallState; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -75,6 +74,8 @@ public class CallRecorder implements CallList.Listener { @Override public void onServiceConnected(ComponentName name, IBinder service) { CallRecorder.this.service = ICallRecorderService.Stub.asInterface(service); + // Helpdesk patch: if a call is already active when the recorder binds, auto-start recording. + handler.post(CallRecorder.this::maybeAutoStart); } @Override @@ -116,11 +117,41 @@ public class CallRecorder implements CallList.Listener { } } + // Helpdesk patch: auto-record the active call. Called when a call goes active AND when the recorder + // service binds (whichever is later); isRecording() guards against a double start. Bypasses the + // manual record button (CallButtonPresenter) so no per-call consent dialog is shown. + private void maybeAutoStart() { + if (service == null || isRecording()) { + return; + } + DialerCall active = CallList.getInstance().getActiveCall(); + if (active != null && startRecording(active.getNumber(), active.getCreationTimeMillis())) { + // Helpdesk patch: remember which call this recording belongs to (for the upload). + com.android.dialer.helpdesk.HelpdeskRecordingBridge.get().onRecordingStarted(active.getId()); + } + } + public boolean startRecording(final String phoneNumber, final long creationTime) { if (service == null) { return false; } + // Helpdesk patch: promote the recorder to a STARTED foreground service before recording. The + // service is otherwise bound-only, and on targetSdk<=34 a bound service promoted via + // startForeground() records its while-in-use reason into ServiceRecord.mAllowWiu_byBindings — + // a field the legacy capability calculation ignores — so the mic-FGS never receives + // PROCESS_CAPABILITY_FOREGROUND_MICROPHONE and (with RECORD_AUDIO pinned to MODE_FOREGROUND on + // GrapheneOS) AudioRecord is silently fed zeros whenever the InCall UI is not TOP (screen off / + // phone at ear). Starting the service here routes the reason into mAllowWiu_noBinding, which + // the legacy calc does read, so the mic capability is granted and persists for the FGS lifetime + // across the proximity screen-blank. AMS handles this synchronously before the binder call + // below, so the started-state exists by the time startForeground() runs in the service. + try { + context.startForegroundService(new Intent(context, CallRecorderServiceV2.class)); + } catch (Exception e) { + Log.w(TAG, "startForegroundService for recorder failed; continuing with bound-only FGS", e); + } + try { if (service.startRecording(phoneNumber, creationTime)) { for (RecordingProgressListener l : progressListeners) { @@ -170,6 +201,8 @@ public class CallRecorder implements CallList.Listener { try { final CallRecording recording = service.stopRecording(); if (recording != null) { + // Helpdesk patch: upload the finalized recording to the backend (feeds transcription). + com.android.dialer.helpdesk.HelpdeskRecordingBridge.get().onRecordingFinalized(recording.mediaId); if (!TextUtils.isEmpty(recording.phoneNumber)) { String msg = context.getResources().getString(R.string.call_recording_file_location, recording.fileName); Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); @@ -197,19 +230,18 @@ public class CallRecorder implements CallList.Listener { if (!initialized && callList.getActiveCall() != null) { // we'll come here if this is the first active call initialize(); - } else { - // we can come down this branch to resume a call that was on hold - CallRecording active = getActiveRecording(); - if (active != null) { - DialerCall call = - callList.getCallWithStateAndNumber(DialerCallState.ONHOLD, active.phoneNumber); - if (call != null) { - // The call associated with the active recording has been placed - // on hold, so stop the recording. - finishRecording(); - } - } + // Helpdesk patch: auto-record on answer (~500ms later, once the recorder service has bound). + handler.postDelayed(this::maybeAutoStart, UPDATE_INTERVAL); } + // Helpdesk patch: do NOT stop recording when the call is placed on hold. Upstream stopped the + // recording here (and, in our build, that also finalized + uploaded it). But calling stopRecording() + // at the exact moment the VOICE_CALL audio path is torn down for the hold can leave the MP4 without + // its moov atom — a truncated, un-transcribable file — which then gets uploaded (the backend rejects + // it with "file is not a readable audio file"). Instead we keep the recorder running across holds and + // finalize exactly once, on disconnect (see onDisconnect), so a held-then-resumed call stays one + // continuous recording and the finalize happens after the audio path has settled. The recorder + // tolerates the silent hold gap (BaseCallRecorder treats a transient AudioRecord read error during a + // hold as a skip, not a stop). } @Override