diff --git a/Android.mk b/Android.mk
index c842c5e690..6f3038c930 100644
--- a/Android.mk
+++ b/Android.mk
@@ -79,6 +79,8 @@ LOCAL_SRC_FILES += $(call all-proto-files-under, $(BASE_DIR))
LOCAL_SRC_FILES += $(call all-Iaidl-files-under, $(BASE_DIR))
LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
+LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/java
+
LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)
LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(RES_DIRS))
diff --git a/assets/quantum/res/drawable/quantum_ic_record_white_36.xml b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
new file mode 100644
index 0000000000..35aaa4134a
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher.xml b/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher.xml
index a8a8fa5518..5c84730caa 100644
--- a/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher.xml
+++ b/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher.xml
@@ -2,4 +2,5 @@
+
diff --git a/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher_round.xml b/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher_round.xml
index a8a8fa5518..5c84730caa 100644
--- a/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher_round.xml
+++ b/java/com/android/dialer/app/res/mipmap-anydpi/ic_launcher_round.xml
@@ -2,4 +2,5 @@
+
diff --git a/java/com/android/dialer/app/res/values/ids.xml b/java/com/android/dialer/app/res/values/ids.xml
index ca5f854de0..050caddf71 100644
--- a/java/com/android/dialer/app/res/values/ids.xml
+++ b/java/com/android/dialer/app/res/values/ids.xml
@@ -25,5 +25,6 @@
+
diff --git a/java/com/android/dialer/app/res/values/strings_ext.xml b/java/com/android/dialer/app/res/values/strings_ext.xml
new file mode 100644
index 0000000000..b59528fdef
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/strings_ext.xml
@@ -0,0 +1,49 @@
+
+
+
+ Call recording
+
+ Audio output format
+
+
+ - AAC in MPEG-4 container
+ - AMR-WB
+
+
+
+ - "0"
+ - "1"
+
+
+ "0"
+
+ Audio source
+
+
+ - MIC (1)
+ - VOICE_CALL (4)
+
+
+
+ - "1"
+ - "4"
+
+
+ "4"
+
+
diff --git a/java/com/android/dialer/app/res/xml/call_recording_settings.xml b/java/com/android/dialer/app/res/xml/call_recording_settings.xml
new file mode 100644
index 0000000000..72761959c3
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/call_recording_settings.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/com/android/dialer/app/settings/CallRecordingSettingsFragment.java b/java/com/android/dialer/app/settings/CallRecordingSettingsFragment.java
new file mode 100644
index 0000000000..a04e6a98fc
--- /dev/null
+++ b/java/com/android/dialer/app/settings/CallRecordingSettingsFragment.java
@@ -0,0 +1,15 @@
+package com.android.dialer.app.settings;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import com.android.dialer.app.R;
+import com.android.dialer.callrecord.impl.CallRecorderService;
+
+public class CallRecordingSettingsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.call_recording_settings);
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index d9bcd6ba52..886b54678a 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -105,6 +105,12 @@ public void onBuildHeaders(List target) {
soundSettingsHeader.id = R.id.settings_header_sounds_and_vibration;
target.add(soundSettingsHeader);
+ Header callRecordingHeader = new Header();
+ callRecordingHeader.titleRes = R.string.call_recording_header_title;
+ callRecordingHeader.fragment = CallRecordingSettingsFragment.class.getName();
+ callRecordingHeader.id = R.id.settings_header_call_recording;
+ target.add(callRecordingHeader);
+
Header quickResponseSettingsHeader = new Header();
Intent quickResponseSettingsIntent =
new Intent(TelecomManager.ACTION_SHOW_RESPOND_VIA_SMS_SETTINGS);
diff --git a/java/com/android/dialer/callrecord/AndroidManifest.xml b/java/com/android/dialer/callrecord/AndroidManifest.xml
new file mode 100644
index 0000000000..5e25c7351e
--- /dev/null
+++ b/java/com/android/dialer/callrecord/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/com/android/dialer/callrecord/CallRecording.aidl b/java/com/android/dialer/callrecord/CallRecording.aidl
new file mode 100644
index 0000000000..e8d65bef2a
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.aidl
@@ -0,0 +1,3 @@
+package com.android.dialer.callrecord;
+
+parcelable CallRecording;
diff --git a/java/com/android/dialer/callrecord/CallRecording.java b/java/com/android/dialer/callrecord/CallRecording.java
new file mode 100644
index 0000000000..3d7ee10857
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callrecord;
+
+import android.content.ContentValues;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public final class CallRecording implements Parcelable {
+ public String phoneNumber;
+ public long creationTime;
+ public String fileName;
+ public long startRecordingTime;
+ public long mediaId;
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ @Override
+ public CallRecording createFromParcel(Parcel in) {
+ return new CallRecording(in);
+ }
+
+ @Override
+ public CallRecording[] newArray(int size) {
+ return new CallRecording[size];
+ }
+ };
+
+ public CallRecording(String phoneNumber, long creationTime,
+ String fileName, long startRecordingTime, long mediaId) {
+ this.phoneNumber = phoneNumber;
+ this.creationTime = creationTime;
+ this.fileName = fileName;
+ this.startRecordingTime = startRecordingTime;
+ this.mediaId = mediaId;
+ }
+
+ public CallRecording(Parcel in) {
+ phoneNumber = in.readString();
+ creationTime = in.readLong();
+ fileName = in.readString();
+ startRecordingTime = in.readLong();
+ mediaId = in.readLong();
+ }
+
+ public static ContentValues generateMediaInsertValues(String fileName, long creationTime) {
+ final ContentValues cv = new ContentValues(5);
+
+ cv.put(MediaStore.Audio.Media.RELATIVE_PATH, "Recordings/CallRecordings");
+ cv.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);
+ cv.put(MediaStore.Audio.Media.DATE_TAKEN, creationTime);
+ cv.put(MediaStore.Audio.Media.IS_PENDING, 1);
+
+ final String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+ final String mime = !TextUtils.isEmpty(extension)
+ ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+ cv.put(MediaStore.Audio.Media.MIME_TYPE, mime);
+
+ return cv;
+ }
+
+ public static ContentValues generateCompletedValues() {
+ final ContentValues cv = new ContentValues(1);
+ cv.put(MediaStore.Audio.Media.IS_PENDING, 0);
+ return cv;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(phoneNumber);
+ out.writeLong(creationTime);
+ out.writeString(fileName);
+ out.writeLong(startRecordingTime);
+ out.writeLong(mediaId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "phoneNumber=" + phoneNumber + ", creationTime=" + creationTime +
+ ", fileName=" + fileName + ", startRecordingTime=" + startRecordingTime;
+ }
+}
diff --git a/java/com/android/dialer/callrecord/ICallRecorderService.aidl b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
new file mode 100644
index 0000000000..acbd5f8bb5
--- /dev/null
+++ b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
@@ -0,0 +1,37 @@
+package com.android.dialer.callrecord;
+
+import com.android.dialer.callrecord.CallRecording;
+
+/**
+ * Service for recording phone calls. Only one recording may be active at a time
+ * (i.e. every call to startRecording should be followed by a call to stopRecording).
+ */
+interface ICallRecorderService {
+ /**
+ * Start a recording.
+ *
+ * @return true if recording started successfully
+ */
+ boolean startRecording(String phoneNumber, long creationTime);
+
+ /**
+ * stops the current recording
+ *
+ * @return call recording data including the output filename
+ */
+ CallRecording stopRecording();
+
+ /**
+ * Recording status
+ *
+ * @return true if there is an active recording
+ */
+ boolean isRecording();
+
+ /**
+ * Get recording currently in progress
+ *
+ * @return call recording object
+ */
+ CallRecording getActiveRecording();
+}
diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderService.java b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
new file mode 100644
index 0000000000..de9db4b792
--- /dev/null
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callrecord.impl;
+
+import android.app.Service;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Objects;
+
+import com.android.dialer.R;
+
+import static java.lang.Integer.parseInt;
+
+public class CallRecorderService extends Service {
+
+ private static final String TAG = "CallRecorderService";
+ private static final boolean DBG = false;
+
+ private static final String KEY_CALL_RECORDING_AUDIO_SOURCE = "call_recording_audio_source";
+ private static final String KEY_CALL_RECORDING_OUTPUT_FORMAT = "call_recording_output_format";
+
+ private MediaRecorder mMediaRecorder = null;
+ private CallRecording mCurrentRecording = null;
+
+ private SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd-HHmmss");
+
+ private final ICallRecorderService.Stub mBinder = new ICallRecorderService.Stub() {
+ @Override
+ public CallRecording stopRecording() {
+ return stopRecordingInternal();
+ }
+
+ @Override
+ public boolean startRecording(String phoneNumber, long creationTime) throws RemoteException {
+ return startRecordingInternal(phoneNumber, creationTime);
+ }
+
+ @Override
+ public boolean isRecording() throws RemoteException {
+ return mMediaRecorder != null;
+ }
+
+ @Override
+ public CallRecording getActiveRecording() throws RemoteException {
+ return mCurrentRecording;
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ if (DBG) Log.d(TAG, "Creating CallRecorderService");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private SharedPreferences getPrefs() {
+ // This replicates PreferenceManager.getDefaultSharedPreferences, except
+ // that we need multi process preferences, as the pref is written in a separate
+ // process (com.android.dialer vs. com.android.incallui)
+ final String prefName = getPackageName() + "_preferences";
+ return getSharedPreferences(prefName, MODE_MULTI_PROCESS);
+ }
+
+ private int getAudioSource() {
+ String def = getString(R.string.call_recording_audio_source_default);
+ return parseInt(getPrefs().getString(KEY_CALL_RECORDING_AUDIO_SOURCE, def));
+ }
+
+ private static final int OUTPUT_FORMAT_AAC_MPEG_4 = 0;
+ private static final int OUTPUT_FORMAT_AMR_WB = 1;
+
+ private int getOutputFormat() {
+ String def = getString(R.string.call_recording_output_format_default);
+ return parseInt(getPrefs().getString(KEY_CALL_RECORDING_OUTPUT_FORMAT, def));
+ }
+
+ private synchronized boolean startRecordingInternal(String phoneNumber, long creationTime) {
+ if (mMediaRecorder != null) {
+ Log.i(TAG, "Start called with recording in progress, stopping current recording");
+ stopRecordingInternal();
+ }
+
+ if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "Record audio permission not granted, can't record call");
+ return false;
+ }
+
+ Log.i(TAG, "Starting recording");
+
+ final int audioSource = getAudioSource();
+ final int outputFormat = getOutputFormat();
+
+ mMediaRecorder = new MediaRecorder();
+ try {
+ Log.d(TAG, "Creating media recorder with audio source " + audioSource);
+
+ mMediaRecorder.setAudioSource(audioSource);
+ if (outputFormat == OUTPUT_FORMAT_AAC_MPEG_4) {
+ mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+ mMediaRecorder.setAudioSamplingRate(16000);
+ mMediaRecorder.setAudioEncodingBitRate(48000);
+ } else if (outputFormat == OUTPUT_FORMAT_AMR_WB){
+ mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_WB);
+ mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB);
+ mMediaRecorder.setAudioEncodingBitRate(23850);
+ } else {
+ throw new IllegalStateException("unexpected output format " + outputFormat);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Error initializing media recorder", e);
+
+ releaseMediaRecorder();
+
+ return false;
+ }
+
+ String fileName = generateFilename(phoneNumber, outputFormat);
+ Uri uri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ CallRecording.generateMediaInsertValues(fileName, creationTime));
+
+ try {
+ try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w")) {
+ if (pfd == null) {
+ throw new IOException("Opening file for URI " + uri + " failed");
+ }
+ mMediaRecorder.setOutputFile(pfd.getFileDescriptor());
+ mMediaRecorder.prepare();
+ }
+ mMediaRecorder.start();
+
+ long mediaId = Long.parseLong(uri.getLastPathSegment());
+ mCurrentRecording = new CallRecording(phoneNumber, creationTime,
+ fileName, System.currentTimeMillis(), mediaId);
+ return true;
+ } catch (IOException | IllegalStateException e) {
+ Log.e(TAG, "Could not start recording", e);
+ getContentResolver().delete(uri, null, null);
+ } catch (RuntimeException e) {
+ getContentResolver().delete(uri, null, null);
+ // only catch exceptions thrown by the MediaRecorder JNI code
+ if (e.getMessage().indexOf("start failed") >= 0) {
+ Log.e(TAG, "Could not start recording", e);
+ } else {
+ throw e;
+ }
+ }
+
+ releaseMediaRecorder();
+
+ return false;
+ }
+
+ private synchronized CallRecording stopRecordingInternal() {
+ CallRecording recording = mCurrentRecording;
+ Log.d(TAG, "Stopping current recording");
+ if (mMediaRecorder != null) {
+ try {
+ mMediaRecorder.stop();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Exception closing media recorder", e);
+ }
+
+ releaseMediaRecorder();
+
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCurrentRecording.mediaId);
+ getContentResolver().update(uri, CallRecording.generateCompletedValues(), null, null);
+
+ mCurrentRecording = null;
+ }
+ return recording;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (DBG) Log.d(TAG, "Destroying CallRecorderService");
+ }
+
+ private String generateFilename(String number, int outputFormat) {
+ String timestamp = DATE_FORMAT.format(new Date());
+ String extension = (outputFormat == OUTPUT_FORMAT_AAC_MPEG_4) ? ".m4a" : ".amr";
+
+ if (TextUtils.isEmpty(number)) {
+ number = "unknown";
+ }
+
+ // CallRecord_yyyyMMdd-HHmmss_numberextension.amr/m4a
+ return "CallRecord_" + timestamp + "_" + number + extension;
+ }
+
+ private void releaseMediaRecorder() {
+ Objects.requireNonNull(mMediaRecorder);
+ try {
+ mMediaRecorder.reset();
+ } catch (Exception e) {
+ Log.e(TAG, "unable to reset media recorder", e);
+ }
+
+ mMediaRecorder.release();
+
+ mMediaRecorder = null;
+ }
+}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index f2f7a405dc..ca783039ab 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -16,13 +16,18 @@
package com.android.incallui;
+import android.app.AlertDialog;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Trace;
+import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.os.UserManagerCompat;
import android.telecom.CallAudioState;
import android.telecom.PhoneAccountHandle;
+import android.widget.Toast;
import com.android.contacts.common.compat.CallCompat;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
@@ -40,6 +45,7 @@
import com.android.incallui.audiomode.AudioModeProvider;
import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallRecorder;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.CameraDirection;
import com.android.incallui.call.DialerCallListener;
@@ -62,12 +68,33 @@ public class CallButtonPresenter
InCallButtonUiDelegate,
DialerCallListener {
+ private static final String KEY_RECORDING_WARNING_PRESENTED = "recording_warning_presented";
+
private final Context context;
private InCallButtonUi inCallButtonUi;
private DialerCall call;
private boolean isInCallButtonUiReady;
private PhoneAccountHandle otherAccount;
+ private CallRecorder.RecordingProgressListener recordingProgressListener =
+ new CallRecorder.RecordingProgressListener() {
+ @Override
+ public void onStartRecording() {
+ inCallButtonUi.setCallRecordingState(true);
+ inCallButtonUi.setCallRecordingDuration(0);
+ }
+
+ @Override
+ public void onStopRecording() {
+ inCallButtonUi.setCallRecordingState(false);
+ }
+
+ @Override
+ public void onRecordingTimeProgress(final long elapsedTimeMs) {
+ inCallButtonUi.setCallRecordingDuration(elapsedTimeMs);
+ }
+ };
+
public CallButtonPresenter(Context context) {
this.context = context.getApplicationContext();
}
@@ -86,6 +113,9 @@ public void onInCallButtonUiReady(InCallButtonUi ui) {
inCallPresenter.addCanAddCallListener(this);
inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
+ CallRecorder recorder = CallRecorder.getInstance();
+ recorder.addRecordingProgressListener(recordingProgressListener);
+
// Update the buttons state immediately for the current call
onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
isInCallButtonUiReady = true;
@@ -101,6 +131,10 @@ public void onInCallButtonUiUnready() {
InCallPresenter.getInstance().removeDetailsListener(this);
InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
InCallPresenter.getInstance().removeCanAddCallListener(this);
+
+ CallRecorder recorder = CallRecorder.getInstance();
+ recorder.removeRecordingProgressListener(recordingProgressListener);
+
isInCallButtonUiReady = false;
if (call != null) {
@@ -300,6 +334,53 @@ public void showDialpadClicked(boolean checked) {
getActivity().showDialpadFragment(checked /* show */, true /* animate */);
}
+ @Override
+ public void callRecordClicked(boolean checked) {
+ CallRecorder recorder = CallRecorder.getInstance();
+ if (checked) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean warningPresented = prefs.getBoolean(KEY_RECORDING_WARNING_PRESENTED, false);
+ if (!warningPresented) {
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.recording_warning_title)
+ .setMessage(R.string.recording_warning_text)
+ .setPositiveButton(R.string.onscreenCallRecordText, (dialog, which) -> {
+ prefs.edit()
+ .putBoolean(KEY_RECORDING_WARNING_PRESENTED, true)
+ .apply();
+
+ startCallRecordingOrAskForPermission();
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ } else {
+ startCallRecordingOrAskForPermission();
+ }
+ } else {
+ if (recorder.isRecording()) {
+ recorder.finishRecording();
+ }
+ }
+ }
+
+ private void startCallRecordingOrAskForPermission() {
+ if (hasAllPermissions(CallRecorder.REQUIRED_PERMISSIONS)) {
+ CallRecorder recorder = CallRecorder.getInstance();
+ recorder.startRecording(call.getNumber(), call.getCreationTimeMillis());
+ } else {
+ inCallButtonUi.requestCallRecordingPermissions(CallRecorder.REQUIRED_PERMISSIONS);
+ }
+ }
+
+ private boolean hasAllPermissions(String[] permissions) {
+ for (String p : permissions) {
+ if (context.checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
@Override
public void changeToVideoClicked() {
LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
@@ -486,6 +567,9 @@ private void updateButtonsState(DialerCall call) {
&& call.getState() != DialerCallState.DIALING
&& call.getState() != DialerCallState.CONNECTING;
+ final CallRecorder recorder = CallRecorder.getInstance();
+ final boolean showCallRecordOption = !isVideo && call.getState() == DialerCallState.ACTIVE;
+
otherAccount = TelecomUtil.getOtherAccount(getContext(), call.getAccountHandle());
boolean showSwapSim =
!call.isEmergencyCall()
@@ -519,6 +603,7 @@ private void updateButtonsState(DialerCall call) {
}
inCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
inCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
+ inCallButtonUi.showButton(InCallButtonIds.BUTTON_RECORD_CALL, showCallRecordOption);
inCallButtonUi.updateButtonStates();
}
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
index b9d0eccbaf..b2d318f5d0 100644
--- a/java/com/android/incallui/InCallServiceImpl.java
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -27,6 +27,7 @@
import com.android.dialer.feedback.FeedbackComponent;
import com.android.incallui.audiomode.AudioModeProvider;
import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallRecorder;
import com.android.incallui.call.ExternalCallList;
import com.android.incallui.call.TelecomAdapter;
import com.android.incallui.speakeasy.SpeakEasyCallManager;
@@ -112,6 +113,7 @@ public IBinder onBind(Intent intent) {
InCallPresenter.getInstance().onServiceBind();
InCallPresenter.getInstance().maybeStartRevealAnimation(intent);
TelecomAdapter.getInstance().setInCallService(this);
+ CallRecorder.getInstance().setUp(context);
returnToCallController =
new ReturnToCallController(this, ContactInfoCache.getInstance(context));
feedbackListener = FeedbackComponent.get(context).getCallFeedbackListener();
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 634a302a21..8428c8484e 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -26,6 +26,7 @@
import android.telecom.Call;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
+import android.text.TextUtils;
import android.util.ArrayMap;
import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
import com.android.dialer.common.Assert;
@@ -535,6 +536,15 @@ public DialerCall getCallWithState(int state, int positionToFind) {
return retval;
}
+ public DialerCall getCallWithStateAndNumber(int state, String number) {
+ for (DialerCall call : callById.values()) {
+ if (TextUtils.equals(call.getNumber(), number) && call.getState() == state) {
+ return call;
+ }
+ }
+ return null;
+ }
+
/**
* Return if there is any active or background call which was not a parent call (never had a child
* call)
diff --git a/java/com/android/incallui/call/CallRecorder.java b/java/com/android/incallui/call/CallRecorder.java
new file mode 100644
index 0000000000..bcf689baf7
--- /dev/null
+++ b/java/com/android/incallui/call/CallRecorder.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.call;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.XmlResourceParser;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.dialer.R;
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+import com.android.dialer.callrecord.impl.CallRecorderService;
+import com.android.dialer.location.GeoUtil;
+import com.android.incallui.call.state.DialerCallState;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+/**
+ * InCall UI's interface to the call recorder
+ *
+ * Manages the call recorder service lifecycle. We bind to the service whenever an active call
+ * is established, and unbind when all calls have been disconnected.
+ */
+public class CallRecorder implements CallList.Listener {
+ public static final String TAG = "CallRecorder";
+
+ public static final String[] REQUIRED_PERMISSIONS = new String[] {
+ android.Manifest.permission.RECORD_AUDIO,
+ };
+
+ private static final int UPDATE_INTERVAL = 500;
+
+ private static CallRecorder instance = null;
+ private Context context;
+ private boolean initialized = false;
+ private ICallRecorderService service = null;
+
+ private HashSet progressListeners =
+ new HashSet();
+ private Handler handler = new Handler();
+
+ private ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ CallRecorder.this.service = ICallRecorderService.Stub.asInterface(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ CallRecorder.this.service = null;
+ }
+ };
+
+ public static CallRecorder getInstance() {
+ if (instance == null) {
+ instance = new CallRecorder();
+ }
+ return instance;
+ }
+
+ private CallRecorder() {
+ CallList.getInstance().addListener(this);
+ }
+
+ public void setUp(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ private void initialize() {
+ if (!initialized) {
+ Intent serviceIntent = new Intent(context, CallRecorderService.class);
+ context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+ initialized = true;
+ }
+ }
+
+ private void uninitialize() {
+ if (initialized) {
+ context.unbindService(connection);
+ initialized = false;
+ }
+ }
+
+ public boolean startRecording(final String phoneNumber, final long creationTime) {
+ if (service == null) {
+ return false;
+ }
+
+ try {
+ if (service.startRecording(phoneNumber, creationTime)) {
+ for (RecordingProgressListener l : progressListeners) {
+ l.onStartRecording();
+ }
+ updateRecordingProgressTask.run();
+ return true;
+ } else {
+ Toast.makeText(context, R.string.call_recording_failed_message, Toast.LENGTH_SHORT)
+ .show();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to start recording " + phoneNumber + ", " + new Date(creationTime), e);
+ }
+
+ return false;
+ }
+
+ public boolean isRecording() {
+ if (service == null) {
+ return false;
+ }
+
+ try {
+ return service.isRecording();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Exception checking recording status", e);
+ }
+ return false;
+ }
+
+ public CallRecording getActiveRecording() {
+ if (service == null) {
+ return null;
+ }
+
+ try {
+ return service.getActiveRecording();
+ } catch (RemoteException e) {
+ Log.w("Exception getting active recording", e);
+ }
+ return null;
+ }
+
+ public void finishRecording() {
+ if (service != null) {
+ try {
+ final CallRecording recording = service.stopRecording();
+ if (recording != null) {
+ 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();
+ }
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to stop recording", e);
+ }
+ }
+
+ for (RecordingProgressListener l : progressListeners) {
+ l.onStopRecording();
+ }
+ handler.removeCallbacks(updateRecordingProgressTask);
+ }
+
+ //
+ // Call list listener methods.
+ //
+ @Override
+ public void onIncomingCall(DialerCall call) { /* do nothing */ }
+
+ @Override
+ public void onCallListChange(final CallList callList) {
+ 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();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onDisconnect(final DialerCall call) {
+ CallRecording active = getActiveRecording();
+ if (active != null && TextUtils.equals(call.getNumber(), active.phoneNumber)) {
+ // finish the current recording if the call gets disconnected
+ finishRecording();
+ }
+
+ // tear down the service if there are no more active calls
+ if (CallList.getInstance().getActiveCall() == null) {
+ uninitialize();
+ }
+ }
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) { /* do nothing */ }
+
+ @Override
+ public void onSessionModificationStateChange(DialerCall call) { /* do nothing */ }
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) { /* do nothing */ }
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) { /* do nothing */ }
+
+ @Override
+ public void onInternationalCallOnWifi(DialerCall call) { /* do nothing */ }
+
+ // allow clients to listen for recording progress updates
+ public interface RecordingProgressListener {
+ void onStartRecording();
+ void onStopRecording();
+ void onRecordingTimeProgress(long elapsedTimeMs);
+ }
+
+ public void addRecordingProgressListener(RecordingProgressListener listener) {
+ progressListeners.add(listener);
+ }
+
+ public void removeRecordingProgressListener(RecordingProgressListener listener) {
+ progressListeners.remove(listener);
+ }
+
+ private Runnable updateRecordingProgressTask = new Runnable() {
+ @Override
+ public void run() {
+ CallRecording active = getActiveRecording();
+ if (active != null) {
+ long elapsed = System.currentTimeMillis() - active.startRecordingTime;
+ for (RecordingProgressListener l : progressListeners) {
+ l.onRecordingTimeProgress(elapsed);
+ }
+ }
+ handler.postDelayed(this, UPDATE_INTERVAL);
+ }
+ };
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
index 757d813528..733dcf96d0 100644
--- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
+++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
@@ -117,9 +117,10 @@ private static Map createCommonMapping() {
mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build());
mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build());
mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build());
- mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(5).build());
- mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build());
- mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(4).build());
+ mapping.put(InCallButtonIds.BUTTON_RECORD_CALL, MappingInfo.builder(3).build());
+ mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(4).setSlotOrder(5).build());
+ mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(4).build());
+ mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(5).build());
return mapping;
}
}
diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java
index 328ebbe67c..7c15f7e0d1 100644
--- a/java/com/android/incallui/incall/impl/ButtonController.java
+++ b/java/com/android/incallui/incall/impl/ButtonController.java
@@ -16,6 +16,7 @@
package com.android.incallui.incall.impl;
+import android.content.res.Resources;
import android.graphics.drawable.AnimationDrawable;
import android.support.annotation.CallSuper;
import android.support.annotation.DrawableRes;
@@ -23,6 +24,7 @@
import android.support.annotation.StringRes;
import android.telecom.CallAudioState;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.view.View;
import android.view.View.OnClickListener;
import com.android.dialer.common.Assert;
@@ -411,6 +413,95 @@ public void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, bool
}
}
+ class CallRecordButtonController implements ButtonController, OnClickListener {
+ @NonNull private final InCallButtonUiDelegate delegate;
+ private boolean isEnabled = true; // We're not going to limit how users use call recording.
+ private boolean isAllowed = true; // We're not going to limit how users use call recording.
+ private boolean isChecked;
+ private long recordingSeconds;
+ private CheckableLabeledButton button;
+
+ public CallRecordButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ if (button != null) {
+ button.setChecked(isChecked);
+ }
+ }
+
+ @Override
+ public int getInCallButtonId() {
+ return InCallButtonIds.BUTTON_RECORD_CALL;
+ }
+
+ @Override
+ public void setButton(CheckableLabeledButton button) {
+ this.button = button;
+ if (button != null) {
+ final Resources res = button.getContext().getResources();
+ if (isChecked) {
+ CharSequence duration = DateUtils.formatElapsedTime(recordingSeconds);
+ button.setLabelText(res.getString(R.string.onscreenCallRecordingText, duration));
+ } else {
+ button.setLabelText(R.string.onscreenCallRecordText);
+ }
+ button.setEnabled(isEnabled);
+ button.setVisibility(View.VISIBLE);
+ button.setChecked(isChecked);
+ button.setOnClickListener(this);
+ button.setIconDrawable(R.drawable.quantum_ic_record_white_36);
+ button.setContentDescription(res.getText(
+ isChecked ? R.string.onscreenStopCallRecordText : R.string.onscreenCallRecordText));
+ button.setShouldShowMoreIndicator(false);
+ }
+ }
+
+ public void setRecordingState(boolean recording) {
+ isChecked = recording;
+ setButton(button);
+ }
+
+ public void setRecordingDuration(long durationMs) {
+ recordingSeconds = (durationMs + 500) / 1000;
+ setButton(button);
+ }
+
+ @Override
+ public void onClick(View v) {
+ delegate.callRecordClicked(!isChecked);
+ }
+ }
+
class DialpadButtonController extends SimpleCheckableButtonController {
public DialpadButtonController(@NonNull InCallButtonUiDelegate delegate) {
diff --git a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
index bfc2781a9f..ec932b9dc4 100644
--- a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
+++ b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
@@ -156,6 +156,10 @@ public void setLabelText(@StringRes int stringRes) {
labelView.setText(stringRes);
}
+ public void setLabelText(CharSequence label) {
+ labelView.setText(label);
+ }
+
/** Shows or hides a little down arrow to indicate that the button will pop up a menu. */
public void setShouldShowMoreIndicator(boolean shouldShow) {
iconView.setBackground(shouldShow ? backgroundMore : background);
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index 30620699ac..336550deb3 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -54,6 +54,7 @@
import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
import com.android.incallui.contactgrid.ContactGridManager;
import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.impl.ButtonController.CallRecordButtonController;
import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
import com.android.incallui.incall.impl.ButtonController.UpgradeToRttButtonController;
import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
@@ -95,6 +96,8 @@ public class InCallFragment extends Fragment
private int phoneType;
private boolean stateRestored;
+ private static final int REQUEST_CODE_CALL_RECORD_PERMISSION = 1000;
+
// Add animation to educate users. If a call has enriched calling attachments then we'll
// initially show the attachment page. After a delay seconds we'll animate to the button grid.
private final Handler handler = new Handler();
@@ -117,7 +120,8 @@ private static boolean isSupportedButton(@InCallButtonIds int id) {
|| id == InCallButtonIds.BUTTON_MERGE
|| id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE
|| id == InCallButtonIds.BUTTON_SWAP_SIM
- || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT;
+ || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT
+ || id == InCallButtonIds.BUTTON_RECORD_CALL;
}
@Override
@@ -232,6 +236,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) {
new ButtonController.ManageConferenceButtonController(inCallScreenDelegate));
buttonControllers.add(
new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate));
+ buttonControllers.add(new ButtonController.CallRecordButtonController(inCallButtonUiDelegate));
inCallScreenDelegate.onInCallScreenDelegateInit(this);
inCallScreenDelegate.onInCallScreenReady();
@@ -466,6 +471,39 @@ public void setAudioState(CallAudioState audioState) {
getButtonController(InCallButtonIds.BUTTON_MUTE).setChecked(audioState.isMuted());
}
+ @Override
+ public void setCallRecordingState(boolean isRecording) {
+ ((CallRecordButtonController) getButtonController(InCallButtonIds.BUTTON_RECORD_CALL))
+ .setRecordingState(isRecording);
+ }
+
+ @Override
+ public void setCallRecordingDuration(long durationMs) {
+ ((CallRecordButtonController) getButtonController(InCallButtonIds.BUTTON_RECORD_CALL))
+ .setRecordingDuration(durationMs);
+ }
+
+ @Override
+ public void requestCallRecordingPermissions(String[] permissions) {
+ requestPermissions(permissions, REQUEST_CODE_CALL_RECORD_PERMISSION);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_CODE_CALL_RECORD_PERMISSION) {
+ boolean allGranted = grantResults.length > 0;
+ for (int i = 0; i < grantResults.length; i++) {
+ allGranted &= grantResults[i] == PackageManager.PERMISSION_GRANTED;
+ }
+ if (allGranted) {
+ inCallButtonUiDelegate.callRecordClicked(true);
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
@Override
public void updateButtonStates() {
// When the incall screen is ready, this method is called from #setSecondary, even though the
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
index 80ea75e998..2c956c8be3 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
@@ -38,6 +38,7 @@
InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
InCallButtonIds.BUTTON_SWAP_SIM,
+ InCallButtonIds.BUTTON_RECORD_CALL,
InCallButtonIds.BUTTON_COUNT,
InCallButtonIds.BUTTON_UPGRADE_TO_RTT
})
@@ -58,6 +59,7 @@
int BUTTON_MANAGE_VOICE_CONFERENCE = 12;
int BUTTON_SWITCH_TO_SECONDARY = 13;
int BUTTON_SWAP_SIM = 14;
- int BUTTON_COUNT = 15;
+ int BUTTON_RECORD_CALL = 15;
int BUTTON_UPGRADE_TO_RTT = 16;
+ int BUTTON_COUNT = 17;
}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
index 5239d9d344..ee03c3d412 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
@@ -58,6 +58,8 @@ public static String toString(@InCallButtonIds int id) {
return "SWAP_SIM";
} else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT) {
return "UPGRADE_TO_RTT";
+ } else if (id == InCallButtonIds.BUTTON_RECORD_CALL) {
+ return "RECORD_CALL";
} else {
return "INVALID_BUTTON: " + id;
}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUi.java b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
index 28dd84c421..395dd7e5f4 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUi.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
@@ -37,6 +37,12 @@ public interface InCallButtonUi {
void setAudioState(CallAudioState audioState);
+ default void setCallRecordingState(boolean isRecording) {}
+
+ default void setCallRecordingDuration(long durationMs) {}
+
+ default void requestCallRecordingPermissions(String[] permissions) {}
+
/**
* Once showButton() has been called on each of the individual buttons in the UI, call this to
* configure the overflow menu appropriately.
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
index 4cf40ef6a1..b88c724bcc 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
@@ -65,5 +65,7 @@ public interface InCallButtonUiDelegate {
void swapSimClicked();
+ default void callRecordClicked(boolean checked) {}
+
Context getContext();
}
diff --git a/java/com/android/incallui/res/values/strings_ext.xml b/java/com/android/incallui/res/values/strings_ext.xml
new file mode 100644
index 0000000000..b1a22a262f
--- /dev/null
+++ b/java/com/android/incallui/res/values/strings_ext.xml
@@ -0,0 +1,33 @@
+
+
+
+ Failed to start call recording
+ Saved call recording to %1$s
+
+ Record call
+ Recording call - %1$s
+ Stop recording
+
+ Call recording notice
+ Recording
+ Enable call recording?
+ Warning:
+ You are fully responsible for compliance with any laws, regulations and rules that apply to the use of call recording functionality and the use or distribution of those recordings.
+ By continuing to use call recording, you acknowledge that you fully read this and GrapheneOS will not be held liable in any way for any possible consequences should there be any
+ due to your usage of the call recording feature.
+
+
diff --git a/java/com/android/incallui/theme/res/values-night/colors.xml b/java/com/android/incallui/theme/res/values-night/colors.xml
new file mode 100644
index 0000000000..83357c0461
--- /dev/null
+++ b/java/com/android/incallui/theme/res/values-night/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
diff --git a/java/com/android/incallui/theme/res/values/styles.xml b/java/com/android/incallui/theme/res/values/styles.xml
index 5b65cc3d91..09bb1dd3eb 100644
--- a/java/com/android/incallui/theme/res/values/styles.xml
+++ b/java/com/android/incallui/theme/res/values/styles.xml
@@ -30,7 +30,7 @@
- @null
- @drawable/incall_background_gradient
- - true
+ - false