diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/incallui/Call.java | 5 | ||||
-rw-r--r-- | src/com/android/incallui/CallButtonFragment.java | 48 | ||||
-rw-r--r-- | src/com/android/incallui/CallButtonPresenter.java | 69 | ||||
-rw-r--r-- | src/com/android/incallui/CallCardFragment.java | 46 | ||||
-rw-r--r-- | src/com/android/incallui/CallList.java | 14 | ||||
-rw-r--r-- | src/com/android/incallui/CallRecorder.java | 270 | ||||
-rw-r--r-- | src/com/android/incallui/InCallServiceImpl.java | 1 | ||||
-rw-r--r-- | src/com/android/incallui/Presenter.java | 10 |
8 files changed, 459 insertions, 4 deletions
diff --git a/src/com/android/incallui/Call.java b/src/com/android/incallui/Call.java index f769f398..7f232c10 100644 --- a/src/com/android/incallui/Call.java +++ b/src/com/android/incallui/Call.java @@ -586,6 +586,11 @@ public class Call { 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 2b6d0f75..ff5329bd 100644 --- a/src/com/android/incallui/CallButtonFragment.java +++ b/src/com/android/incallui/CallButtonFragment.java @@ -18,7 +18,9 @@ package com.android.incallui; 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 @@ public class CallButtonFragment // 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 class CallButtonFragment 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 @@ public class CallButtonFragment 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 @@ public class CallButtonFragment 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 @@ public class CallButtonFragment 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 @@ public class CallButtonFragment mShowDialpadButton, mHoldButton, mSwitchCameraButton, - mPauseVideoButton + mPauseVideoButton, + mCallRecordButton }; for (View button : compoundButtons) { @@ -359,6 +371,7 @@ public class CallButtonFragment 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 @@ public class CallButtonFragment 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 @@ public class CallButtonFragment } } + @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 @@ public class CallButtonFragment } @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 a309b566..ce3f27d4 100644 --- a/src/com/android/incallui/CallButtonPresenter.java +++ b/src/com/android/incallui/CallButtonPresenter.java @@ -18,7 +18,11 @@ package com.android.incallui; 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 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto 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 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto 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 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto 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 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto 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 @@ public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButto 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 57372f72..ecc26944 100644 --- a/src/com/android/incallui/CallCardFragment.java +++ b/src/com/android/incallui/CallCardFragment.java @@ -130,6 +130,8 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr // 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 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr */ 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 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr 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 0b4f11a9..6ff5c991 100644 --- a/src/com/android/incallui/CallList.java +++ b/src/com/android/incallui/CallList.java @@ -21,13 +21,14 @@ import android.os.Message; 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 @@ public class CallList { 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 00000000..cf04d950 --- /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 230a2cc1..b201e78c 100644 --- a/src/com/android/incallui/InCallServiceImpl.java +++ b/src/com/android/incallui/InCallServiceImpl.java @@ -82,6 +82,7 @@ public class InCallServiceImpl extends InCallService { 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 4e1fa978..b0ad9fdb 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 abstract class Presenter<U extends Ui> { 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); + } } |