Re-add call recording feature.

Change-Id: I44f766b2ef52d76ace65a9603401ba3368f674b1
diff --git a/res/drawable-hdpi/ic_recording_indicator.png b/res/drawable-hdpi/ic_recording_indicator.png
new file mode 100644
index 0000000..a98b837
--- /dev/null
+++ b/res/drawable-hdpi/ic_recording_indicator.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_toolbar_record.png b/res/drawable-hdpi/ic_toolbar_record.png
new file mode 100644
index 0000000..b395cdb
--- /dev/null
+++ b/res/drawable-hdpi/ic_toolbar_record.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_recording_indicator.png b/res/drawable-mdpi/ic_recording_indicator.png
new file mode 100644
index 0000000..2a4c19e
--- /dev/null
+++ b/res/drawable-mdpi/ic_recording_indicator.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_toolbar_record.png b/res/drawable-mdpi/ic_toolbar_record.png
new file mode 100644
index 0000000..4a99405
--- /dev/null
+++ b/res/drawable-mdpi/ic_toolbar_record.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_recording_indicator.png b/res/drawable-xhdpi/ic_recording_indicator.png
new file mode 100644
index 0000000..33e6875
--- /dev/null
+++ b/res/drawable-xhdpi/ic_recording_indicator.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_toolbar_record.png b/res/drawable-xhdpi/ic_toolbar_record.png
new file mode 100644
index 0000000..b2fc680
--- /dev/null
+++ b/res/drawable-xhdpi/ic_toolbar_record.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_toolbar_record.png b/res/drawable-xxhdpi/ic_toolbar_record.png
new file mode 100644
index 0000000..b1fd09d
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_toolbar_record.png
Binary files differ
diff --git a/res/drawable/btn_compound_record.xml b/res/drawable/btn_compound_record.xml
new file mode 100644
index 0000000..4042525
--- /dev/null
+++ b/res/drawable/btn_compound_record.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source 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.
+-->
+
+<!-- Layers used to render the "call record" compound button. -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- The standard "compound button" background. -->
+    <item android:id="@+id/compoundBackgroundItem"
+          android:drawable="@drawable/btn_compound_background" />
+
+    <!-- ...and the actual icon on top.  Use an explicit <bitmap> to avoid scaling
+         the icon up to the full size of the button. -->
+    <item>
+        <bitmap android:src="@drawable/ic_toolbar_record"
+                android:gravity="center"
+                android:tint="@color/selectable_icon_tint" />
+    </item>
+
+</layer-list>
diff --git a/res/layout/call_button_fragment.xml b/res/layout/call_button_fragment.xml
index 149e397..e40d47e 100644
--- a/res/layout/call_button_fragment.xml
+++ b/res/layout/call_button_fragment.xml
@@ -151,6 +151,13 @@
             android:contentDescription="@string/onscreenAddParticipant"
             android:visibility="gone" />
 
+        <!-- "Record call" -->
+        <ToggleButton android:id="@+id/callRecordButton"
+            style="@style/InCallCompoundButton"
+            android:background="@drawable/btn_compound_record"
+            android:contentDescription="@string/onscreenCallRecordText"
+            android:visibility="gone" />
+
         <!-- "Overflow" -->
         <ImageButton android:id="@+id/overflowButton"
             style="@style/InCallButton"
diff --git a/res/layout/call_card_fragment.xml b/res/layout/call_card_fragment.xml
index dabba76..ed7031a 100644
--- a/res/layout/call_card_fragment.xml
+++ b/res/layout/call_card_fragment.xml
@@ -78,6 +78,35 @@
             android:background="@android:color/white"
             android:src="@drawable/img_no_image_automirrored" />
 
+        <!-- Call recorder info -->
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom|start"
+            android:layout_marginStart="24dp"
+            android:layout_marginBottom="120dp"
+            android:orientation="horizontal">
+
+            <TextView android:id="@+id/recordingIcon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:drawableEnd="@drawable/ic_recording_indicator"
+                android:visibility="invisible" />
+
+            <TextView android:id="@+id/recordingTime"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="5dp"
+                android:layout_gravity="center"
+                android:text="@string/recording_time_text"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textColor="@color/incall_call_banner_text_color"
+                android:singleLine="true"
+                android:visibility="invisible" />
+
+        </LinearLayout>
+
     </FrameLayout>
 
     <fragment android:name="com.android.incallui.VideoCallFragment"
diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml
index cc9e000..e486a4c 100644
--- a/res/values/cm_strings.xml
+++ b/res/values/cm_strings.xml
@@ -35,4 +35,12 @@
 
     <!-- In-call screen: status label for a call that is held remotely  -->
     <string name="card_title_waiting_call">Call on hold</string>
+
+    <string name="call_recording_failed_message">Failed to start call recording</string>
+    <string name="call_recording_file_location">Saved call recording to <xliff:g id="filename">%s</xliff:g></string>
+    <string name="onscreenCallRecordText">Record call</string>
+    <string name="onscreenStopCallRecordText">Stop recording</string>
+    <string name="recording_time_text">Recording</string>
+    <string name="recording_warning_title">Enable call recording?</string>
+    <string name="recording_warning_text">Notice: You are 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.</string>
 </resources>
diff --git a/src/com/android/incallui/Call.java b/src/com/android/incallui/Call.java
index f769f39..7f232c1 100644
--- a/src/com/android/incallui/Call.java
+++ b/src/com/android/incallui/Call.java
@@ -586,6 +586,11 @@
         return mTelecommCall.getDetails().getConnectTimeMillis();
     }
 
+    /** Gets the time when call was first constructed */
+    public long getCreateTimeMillis() {
+        return mTelecommCall.getDetails().getCreateTimeMillis();
+    }
+
     public boolean isConferenceCall() {
         return hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE);
     }
diff --git a/src/com/android/incallui/CallButtonFragment.java b/src/com/android/incallui/CallButtonFragment.java
index 2b6d0f7..ff5329b 100644
--- a/src/com/android/incallui/CallButtonFragment.java
+++ b/src/com/android/incallui/CallButtonFragment.java
@@ -18,7 +18,9 @@
 
 import static com.android.incallui.CallButtonFragment.Buttons.*;
 
+import android.annotation.NonNull;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
@@ -60,6 +62,8 @@
     // The button has been collapsed into the overflow menu
     private static final int BUTTON_MENU = 3;
 
+    private static final int REQUEST_CODE_CALL_RECORD_PERMISSION = 1000;
+
     public interface Buttons {
         public static final int BUTTON_AUDIO = 0;
         public static final int BUTTON_MUTE = 1;
@@ -72,7 +76,8 @@
         public static final int BUTTON_MERGE = 8;
         public static final int BUTTON_PAUSE_VIDEO = 9;
         public static final int BUTTON_MANAGE_VIDEO_CONFERENCE = 10;
-        public static final int BUTTON_COUNT = 11;
+        public static final int BUTTON_RECORD_CALL = 11;
+        public static final int BUTTON_COUNT = 12;
     }
 
     private SparseIntArray mButtonVisibilityMap = new SparseIntArray(BUTTON_COUNT);
@@ -87,6 +92,7 @@
     private ImageButton mAddCallButton;
     private ImageButton mMergeButton;
     private CompoundButton mPauseVideoButton;
+    private CompoundButton mCallRecordButton;
     private ImageButton mOverflowButton;
     private ImageButton mManageVideoCallConferenceButton;
     private ImageButton mAddParticipantButton;
@@ -151,6 +157,8 @@
         mMergeButton.setOnClickListener(this);
         mPauseVideoButton = (CompoundButton) parent.findViewById(R.id.pauseVideoButton);
         mPauseVideoButton.setOnClickListener(this);
+        mCallRecordButton = (CompoundButton) parent.findViewById(R.id.callRecordButton);
+        mCallRecordButton.setOnClickListener(this);
         mAddParticipantButton = (ImageButton) parent.findViewById(R.id.addParticipant);
         mAddParticipantButton.setOnClickListener(this);
         mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton);
@@ -223,6 +231,9 @@
                 getPresenter().pauseVideoClicked(
                         !mPauseVideoButton.isSelected() /* pause */);
                 break;
+            case R.id.callRecordButton:
+                getPresenter().callRecordClicked(!mCallRecordButton.isSelected());
+                break;
             case R.id.overflowButton:
                 if (mOverflowPopup != null) {
                     mOverflowPopup.show();
@@ -254,7 +265,8 @@
                 mShowDialpadButton,
                 mHoldButton,
                 mSwitchCameraButton,
-                mPauseVideoButton
+                mPauseVideoButton,
+                mCallRecordButton
         };
 
         for (View button : compoundButtons) {
@@ -359,6 +371,7 @@
         mAddCallButton.setEnabled(isEnabled);
         mMergeButton.setEnabled(isEnabled);
         mPauseVideoButton.setEnabled(isEnabled);
+        mCallRecordButton.setEnabled(isEnabled);
         mOverflowButton.setEnabled(isEnabled);
         mManageVideoCallConferenceButton.setEnabled(isEnabled);
         mAddParticipantButton.setEnabled(isEnabled);
@@ -401,6 +414,8 @@
                 return mPauseVideoButton;
             case BUTTON_MANAGE_VIDEO_CONFERENCE:
                 return mManageVideoCallConferenceButton;
+            case BUTTON_RECORD_CALL:
+                return mCallRecordButton;
             default:
                 Log.w(this, "Invalid button id");
                 return null;
@@ -438,6 +453,14 @@
         }
     }
 
+    @Override
+    public void setCallRecordingState(boolean isRecording) {
+        mCallRecordButton.setSelected(isRecording);
+        mCallRecordButton.setContentDescription(getContext().getString(isRecording
+                ? R.string.onscreenStopCallRecordText
+                : R.string.onscreenCallRecordText));
+    }
+
     private void addToOverflowMenu(int id, View button, PopupMenu menu) {
         button.setVisibility(View.GONE);
         menu.getMenu().add(Menu.NONE, id, Menu.NONE, button.getContentDescription());
@@ -807,6 +830,27 @@
     }
 
     @Override
+    public void requestCallRecordingPermission(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) {
+                getPresenter().startCallRecording();
+            }
+        } else {
+            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        }
+    }
+
+    @Override
     public Context getContext() {
         return getActivity();
     }
diff --git a/src/com/android/incallui/CallButtonPresenter.java b/src/com/android/incallui/CallButtonPresenter.java
index a309b56..ce3f27d 100644
--- a/src/com/android/incallui/CallButtonPresenter.java
+++ b/src/com/android/incallui/CallButtonPresenter.java
@@ -18,7 +18,11 @@
 
 import static com.android.incallui.CallButtonFragment.Buttons.*;
 
+import android.app.AlertDialog;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.telecom.CallAudioState;
 import android.telecom.InCallService.VideoCall;
@@ -45,6 +49,7 @@
 
     private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted";
     private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
+    private static final String RECORDING_WARNING_PRESENTED = "recording_warning_presented";
 
     private Call mCall;
     private boolean mAutomaticallyMuted = false;
@@ -343,6 +348,63 @@
         getUi().setVideoPaused(pause);
     }
 
+    public void callRecordClicked(boolean startRecording) {
+        CallRecorder recorder = CallRecorder.getInstance();
+        if (startRecording) {
+            Context context = getUi().getContext();
+            final SharedPreferences prefs = getPrefs(context);
+            boolean warningPresented = prefs.getBoolean(RECORDING_WARNING_PRESENTED, false);
+            if (!warningPresented) {
+                new AlertDialog.Builder(context)
+                        .setTitle(R.string.recording_warning_title)
+                        .setMessage(R.string.recording_warning_text)
+                        .setPositiveButton(R.string.onscreenCallRecordText,
+                                new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                prefs.edit()
+                                        .putBoolean(RECORDING_WARNING_PRESENTED, true)
+                                        .apply();
+                                startCallRecordingOrAskForPermission();
+                            }
+                        })
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .show();
+            } else {
+                startCallRecordingOrAskForPermission();
+            }
+        } else {
+            if (recorder.isRecording()) {
+                recorder.finishRecording();
+            }
+            getUi().setCallRecordingState(recorder.isRecording());
+        }
+    }
+
+    public void startCallRecording() {
+        CallRecorder recorder = CallRecorder.getInstance();
+        recorder.startRecording(mCall.getNumber(), mCall.getCreateTimeMillis());
+        getUi().setCallRecordingState(recorder.isRecording());
+    }
+
+    private void startCallRecordingOrAskForPermission() {
+        if (hasAllPermissions(CallRecorder.REQUIRED_PERMISSIONS)) {
+            startCallRecording();
+        } else {
+            getUi().requestCallRecordingPermission(CallRecorder.REQUIRED_PERMISSIONS);
+        }
+    }
+
+    private boolean hasAllPermissions(String[] permissions) {
+        Context context = getUi().getContext();
+        for (String p : permissions) {
+            if (context.checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private void updateUi(InCallState state, Call call) {
         Log.d(this, "Updating call UI for call: ", call);
 
@@ -398,6 +460,10 @@
         final boolean showAddParticipant = call.can(
                 android.telecom.Call.Details.CAPABILITY_ADD_PARTICIPANT);
 
+        final CallRecorder recorder = CallRecorder.getInstance();
+        boolean showCallRecordOption = recorder.isEnabled()
+                && !isVideo && call.getState() == Call.State.ACTIVE;
+
         ui.showButton(BUTTON_AUDIO, true);
         ui.showButton(BUTTON_SWAP, showSwap);
         ui.showButton(BUTTON_HOLD, showHold);
@@ -409,6 +475,7 @@
         ui.showButton(BUTTON_PAUSE_VIDEO, isVideo && !useExt);
         ui.showButton(BUTTON_DIALPAD, !isVideo || useExt);
         ui.showButton(BUTTON_MERGE, showMerge);
+        ui.showButton(BUTTON_RECORD_CALL, showCallRecordOption);
         ui.enableAddParticipant(showAddParticipant);
 
         ui.updateButtonStates();
@@ -453,6 +520,8 @@
         void enableAddParticipant(boolean show);
         void setAudio(int mode);
         void setSupportedAudio(int mask);
+        void setCallRecordingState(boolean isRecording);
+        void requestCallRecordingPermission(String[] permissions);
         void displayDialpad(boolean on, boolean animate);
         boolean isDialpadVisible();
 
diff --git a/src/com/android/incallui/CallCardFragment.java b/src/com/android/incallui/CallCardFragment.java
index 57372f7..ecc2694 100644
--- a/src/com/android/incallui/CallCardFragment.java
+++ b/src/com/android/incallui/CallCardFragment.java
@@ -130,6 +130,8 @@
     // Container view that houses the primary call information
     private ViewGroup mPrimaryCallInfo;
     private View mCallButtonsContainer;
+    private TextView mRecordingTimeLabel;
+    private TextView mRecordingIcon;
 
     // Secondary caller info
     private View mSecondaryCallInfo;
@@ -171,6 +173,36 @@
      */
     private boolean mHasSecondaryCallInfo = false;
 
+    private CallRecorder.RecordingProgressListener mRecordingProgressListener =
+            new CallRecorder.RecordingProgressListener() {
+        @Override
+        public void onStartRecording() {
+            mRecordingTimeLabel.setText(DateUtils.formatElapsedTime(0));
+            if (mRecordingTimeLabel.getVisibility() != View.VISIBLE) {
+                AnimUtils.fadeIn(mRecordingTimeLabel, AnimUtils.DEFAULT_DURATION);
+            }
+            if (mRecordingIcon.getVisibility() != View.VISIBLE) {
+                AnimUtils.fadeIn(mRecordingIcon, AnimUtils.DEFAULT_DURATION);
+            }
+        }
+
+        @Override
+        public void onStopRecording() {
+            AnimUtils.fadeOut(mRecordingTimeLabel, AnimUtils.DEFAULT_DURATION);
+            AnimUtils.fadeOut(mRecordingIcon, AnimUtils.DEFAULT_DURATION);
+        }
+
+        @Override
+        public void onRecordingTimeProgress(final long elapsedTimeMs) {
+            long elapsedSeconds = (elapsedTimeMs + 500) / 1000;
+            mRecordingTimeLabel.setText(DateUtils.formatElapsedTime(elapsedSeconds));
+
+            // make sure this is visible in case we re-loaded the UI for a call in progress
+            mRecordingTimeLabel.setVisibility(View.VISIBLE);
+            mRecordingIcon.setVisibility(View.VISIBLE);
+        }
+    };
+
     @Override
     public CallCardPresenter.CallCardUi getUi() {
         return this;
@@ -291,6 +323,20 @@
         mPrimaryName.setElegantTextHeight(false);
         mCallStateLabel.setElegantTextHeight(false);
         mCallSubject = (TextView) view.findViewById(R.id.callSubject);
+
+        mRecordingTimeLabel = (TextView) view.findViewById(R.id.recordingTime);
+        mRecordingIcon = (TextView) view.findViewById(R.id.recordingIcon);
+
+        CallRecorder recorder = CallRecorder.getInstance();
+        recorder.addRecordingProgressListener(mRecordingProgressListener);
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+
+        CallRecorder recorder = CallRecorder.getInstance();
+        recorder.removeRecordingProgressListener(mRecordingProgressListener);
     }
 
     @Override
diff --git a/src/com/android/incallui/CallList.java b/src/com/android/incallui/CallList.java
index 0b4f11a..6ff5c99 100644
--- a/src/com/android/incallui/CallList.java
+++ b/src/com/android/incallui/CallList.java
@@ -21,13 +21,14 @@
 import android.os.Trace;
 import android.telecom.DisconnectCause;
 import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+import android.text.TextUtils;
 
 import com.android.contacts.common.testing.NeededForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import android.telecom.PhoneAccountHandle;
-import android.telephony.SubscriptionManager;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -850,6 +851,15 @@
         return retval;
     }
 
+     public Call getCallWithStateAndNumber(int state, String number) {
+         for (Call call : mCallById.values()) {
+             if (TextUtils.equals(call.getNumber(), number) && call.getState() == state) {
+                 return call;
+             }
+         }
+         return null;
+    }
+
     void addActiveSubChangeListener(ActiveSubChangeListener listener) {
         Preconditions.checkNotNull(listener);
         mActiveSubChangeListeners.add(listener);
diff --git a/src/com/android/incallui/CallRecorder.java b/src/com/android/incallui/CallRecorder.java
new file mode 100644
index 0000000..cf04d95
--- /dev/null
+++ b/src/com/android/incallui/CallRecorder.java
@@ -0,0 +1,270 @@
+/*
+ * 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;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+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.services.callrecorder.CallRecorderService;
+import com.android.services.callrecorder.CallRecordingDataStore;
+import com.android.services.callrecorder.common.CallRecording;
+import com.android.services.callrecorder.common.ICallRecorderService;
+
+import java.util.Date;
+import java.util.HashSet;
+
+/**
+ * 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,
+        android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+    };
+
+    private static CallRecorder sInstance = null;
+
+    private Context mContext;
+    private boolean mInitialized = false;
+    private ICallRecorderService mService = null;
+
+    private HashSet<RecordingProgressListener> mProgressListeners =
+            new HashSet<RecordingProgressListener>();
+    private Handler mHandler = new Handler();
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mService = ICallRecorderService.Stub.asInterface(service);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            mService = null;
+        }
+    };
+
+    public static CallRecorder getInstance() {
+        if (sInstance == null) {
+            sInstance = new CallRecorder();
+        }
+        return sInstance;
+    }
+
+    public boolean isEnabled() {
+        return CallRecorderService.isEnabled(mContext);
+    }
+
+    private CallRecorder() {
+        CallList.getInstance().addListener(this);
+    }
+
+    public void setUp(Context context) {
+        mContext = context.getApplicationContext();
+    }
+
+    private void initialize() {
+        if (isEnabled() && !mInitialized) {
+            Intent serviceIntent = new Intent(mContext, CallRecorderService.class);
+            mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
+            mInitialized = true;
+        }
+    }
+
+    private void uninitialize() {
+        if (mInitialized) {
+            mContext.unbindService(mConnection);
+            mInitialized = false;
+        }
+    }
+
+    public boolean startRecording(final String phoneNumber, final long creationTime) {
+        if (mService == null) {
+            return false;
+        }
+
+        try {
+            if (mService.startRecording(phoneNumber, creationTime)) {
+                for (RecordingProgressListener l : mProgressListeners) {
+                    l.onStartRecording();
+                }
+                mUpdateRecordingProgressTask.run();
+                return true;
+            } else {
+                Toast.makeText(mContext, 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 (mService == null) {
+            return false;
+        }
+
+        try {
+            return mService.isRecording();
+        } catch (RemoteException e) {
+            Log.w(TAG, "Exception checking recording status", e);
+        }
+        return false;
+    }
+
+    public CallRecording getActiveRecording() {
+        if (mService == null) {
+            return null;
+        }
+
+        try {
+            return mService.getActiveRecording();
+        } catch (RemoteException e) {
+            Log.w("Exception getting active recording", e);
+        }
+        return null;
+    }
+
+    public void finishRecording() {
+        if (mService != null) {
+            try {
+                final CallRecording recording = mService.stopRecording();
+                if (recording != null) {
+                    if (!TextUtils.isEmpty(recording.phoneNumber)) {
+                        new Thread(new Runnable() {
+                            @Override
+                            public void run() {
+                                CallRecordingDataStore dataStore = new CallRecordingDataStore();
+                                dataStore.open(mContext);
+                                dataStore.putRecording(recording);
+                                dataStore.close();
+                            }
+                        }).start();
+                    } else {
+                        // Data store is an index by number so that we can link recordings in the
+                        // call detail page.  If phone number is not available (conference call or
+                        // unknown number) then just display a toast.
+                        String msg = mContext.getResources().getString(
+                                R.string.call_recording_file_location, recording.fileName);
+                        Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
+                    }
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to stop recording", e);
+            }
+        }
+
+        for (RecordingProgressListener l : mProgressListeners) {
+            l.onStopRecording();
+        }
+        mHandler.removeCallbacks(mUpdateRecordingProgressTask);
+    }
+
+    //
+    // Call list listener methods.
+    //
+    @Override
+    public void onIncomingCall(Call call) {
+        // do nothing
+    }
+
+    @Override
+    public void onCallListChange(final CallList callList) {
+        if (!mInitialized && 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) {
+                Call call = callList.getCallWithStateAndNumber(Call.State.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 Call 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(Call call) {}
+
+    // allow clients to listen for recording progress updates
+    public interface RecordingProgressListener {
+        public void onStartRecording();
+        public void onStopRecording();
+        public void onRecordingTimeProgress(long elapsedTimeMs);
+    }
+
+    public void addRecordingProgressListener(RecordingProgressListener listener) {
+        mProgressListeners.add(listener);
+    }
+
+    public void removeRecordingProgressListener(RecordingProgressListener listener) {
+        mProgressListeners.remove(listener);
+    }
+
+    private static final int UPDATE_INTERVAL = 500;
+
+    private Runnable mUpdateRecordingProgressTask = new Runnable() {
+        @Override
+        public void run() {
+            CallRecording active = getActiveRecording();
+            if (active != null) {
+                long elapsed = System.currentTimeMillis() - active.startRecordingTime;
+                for (RecordingProgressListener l : mProgressListeners) {
+                    l.onRecordingTimeProgress(elapsed);
+                }
+            }
+            mHandler.postDelayed(mUpdateRecordingProgressTask, UPDATE_INTERVAL);
+        }
+    };
+}
diff --git a/src/com/android/incallui/InCallServiceImpl.java b/src/com/android/incallui/InCallServiceImpl.java
index 230a2cc..b201e78 100644
--- a/src/com/android/incallui/InCallServiceImpl.java
+++ b/src/com/android/incallui/InCallServiceImpl.java
@@ -82,6 +82,7 @@
         InCallPresenter.getInstance().onServiceBind();
         InCallPresenter.getInstance().maybeStartRevealAnimation(intent);
         TelecomAdapter.getInstance().setInCallService(this);
+        CallRecorder.getInstance().setUp(getApplicationContext());
 
         return super.onBind(intent);
     }
diff --git a/src/com/android/incallui/Presenter.java b/src/com/android/incallui/Presenter.java
index 4e1fa97..b0ad9fd 100644
--- a/src/com/android/incallui/Presenter.java
+++ b/src/com/android/incallui/Presenter.java
@@ -17,6 +17,8 @@
 package com.android.incallui;
 
 import android.os.Bundle;
+import android.content.Context;
+import android.content.SharedPreferences;
 
 /**
  * Base class for Presenters.
@@ -56,4 +58,12 @@
     public U getUi() {
         return mUi;
     }
+
+    public static SharedPreferences getPrefs(Context context) {
+        // 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 = context.getPackageName() + "_preferences";
+        return context.getSharedPreferences(prefName, Context.MODE_MULTI_PROCESS);
+    }
 }