diff options
Diffstat (limited to 'java/com/android/incallui')
17 files changed, 645 insertions, 5 deletions
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java index 2a9600a2b..cff283c21 100644 --- a/java/com/android/incallui/CallButtonPresenter.java +++ b/java/com/android/incallui/CallButtonPresenter.java @@ -16,9 +16,13 @@ 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; @@ -40,6 +44,7 @@ import com.android.incallui.InCallPresenter.IncomingCallListener; 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 +67,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 +112,9 @@ public class CallButtonPresenter 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 +130,10 @@ public class CallButtonPresenter 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) { @@ -297,6 +330,52 @@ public class CallButtonPresenter } @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"); Logger.get(context) @@ -482,6 +561,10 @@ public class CallButtonPresenter && call.getState() != DialerCallState.DIALING && call.getState() != DialerCallState.CONNECTING; + final CallRecorder recorder = CallRecorder.getInstance(); + final boolean showCallRecordOption = recorder.canRecordInCurrentCountry() + && !isVideo && call.getState() == DialerCallState.ACTIVE; + otherAccount = TelecomUtil.getOtherAccount(getContext(), call.getAccountHandle()); boolean showSwapSim = !call.isEmergencyCall() @@ -515,6 +598,7 @@ public class CallButtonPresenter } 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 b9d0eccba..b2d318f5d 100644 --- a/java/com/android/incallui/InCallServiceImpl.java +++ b/java/com/android/incallui/InCallServiceImpl.java @@ -27,6 +27,7 @@ import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 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 class InCallServiceImpl extends InCallService { 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 634a302a2..8428c8484 100644 --- a/java/com/android/incallui/call/CallList.java +++ b/java/com/android/incallui/call/CallList.java @@ -26,6 +26,7 @@ import android.support.annotation.VisibleForTesting; 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 class CallList implements DialerCallDelegate { 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 000000000..867d5a57c --- /dev/null +++ b/java/com/android/incallui/call/CallRecorder.java @@ -0,0 +1,336 @@ +/* + * 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.CallRecordingDataStore; +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, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + private static final HashMap<String, Boolean> RECORD_ALLOWED_STATE_BY_COUNTRY = new HashMap<>(); + + private static CallRecorder instance = null; + private Context context; + private boolean initialized = false; + private ICallRecorderService service = null; + + private HashSet<RecordingProgressListener> progressListeners = + new HashSet<RecordingProgressListener>(); + 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; + } + + public boolean isEnabled() { + return CallRecorderService.isEnabled(context); + } + + public boolean canRecordInCurrentCountry() { + if (!isEnabled()) { + return false; + } + if (RECORD_ALLOWED_STATE_BY_COUNTRY.isEmpty()) { + loadAllowedStates(); + } + + String currentCountryIso = GeoUtil.getCurrentCountryIso(context); + Boolean allowedState = RECORD_ALLOWED_STATE_BY_COUNTRY.get(currentCountryIso); + + return allowedState != null && allowedState; + } + + private CallRecorder() { + CallList.getInstance().addListener(this); + } + + public void setUp(Context context) { + this.context = context.getApplicationContext(); + } + + private void initialize() { + if (isEnabled() && !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)) { + new Thread(() -> { + CallRecordingDataStore dataStore = new CallRecordingDataStore(); + dataStore.open(context); + 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 = 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) {} + + @Override + public void onSessionModificationStateChange(DialerCall call) {} + + @Override + public void onWiFiToLteHandover(DialerCall call) {} + + @Override + public void onHandoverToWifiFailed(DialerCall call) {} + + @Override + public void onInternationalCallOnWifi(DialerCall call) {} + + // 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 static final int UPDATE_INTERVAL = 500; + + 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); + } + }; + + private void loadAllowedStates() { + XmlResourceParser parser = context.getResources().getXml(R.xml.call_record_states); + try { + // Consume all START_DOCUMENT which can appear more than once. + while (parser.next() == XmlPullParser.START_DOCUMENT) {} + + parser.require(XmlPullParser.START_TAG, null, "call-record-allowed-flags"); + + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + parser.require(XmlPullParser.START_TAG, null, "country"); + + String iso = parser.getAttributeValue(null, "iso"); + String allowed = parser.getAttributeValue(null, "allowed"); + if (iso != null && ("true".equals(allowed) || "false".equals(allowed))) { + for (String splittedIso : iso.split(",")) { + RECORD_ALLOWED_STATE_BY_COUNTRY.put( + splittedIso.toUpperCase(Locale.US), Boolean.valueOf(allowed)); + } + } else { + throw new XmlPullParserException("Unexpected country specification", parser, null); + } + } + Log.d(TAG, "Loaded " + RECORD_ALLOWED_STATE_BY_COUNTRY.size() + " country records"); + } catch (XmlPullParserException | IOException e) { + Log.e(TAG, "Could not parse allowed country list", e); + RECORD_ALLOWED_STATE_BY_COUNTRY.clear(); + } finally { + parser.close(); + } + } +} diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java index 4086e1419..5177783b0 100644 --- a/java/com/android/incallui/callpending/CallPendingActivity.java +++ b/java/com/android/incallui/callpending/CallPendingActivity.java @@ -285,6 +285,9 @@ public class CallPendingActivity extends FragmentActivity public void swapSimClicked() {} @Override + public void callRecordClicked(boolean checked) {} + + @Override public Context getContext() { return CallPendingActivity.this; } diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java index 757d81352..733dcf96d 100644 --- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java +++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java @@ -117,9 +117,10 @@ class ButtonChooserFactory { 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 328ebbe67..2ad3d3e6d 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.NonNull; 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 @@ interface ButtonController { } } + class CallRecordButtonController implements ButtonController, OnClickListener { + @NonNull private final InCallButtonUiDelegate delegate; + private boolean isEnabled; + private boolean isAllowed; + 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(isAllowed ? View.VISIBLE : View.INVISIBLE); + 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 bfc2781a9..ec932b9dc 100644 --- a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java +++ b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java @@ -156,6 +156,10 @@ public class CheckableLabeledButton extends LinearLayout implements Checkable { 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 30620699a..336550deb 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; 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 @@ public class InCallFragment extends Fragment || 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 class InCallFragment extends Fragment new ButtonController.ManageConferenceButtonController(inCallScreenDelegate)); buttonControllers.add( new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate)); + buttonControllers.add(new ButtonController.CallRecordButtonController(inCallButtonUiDelegate)); inCallScreenDelegate.onInCallScreenDelegateInit(this); inCallScreenDelegate.onInCallScreenReady(); @@ -467,6 +472,39 @@ public class InCallFragment extends Fragment } @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 // incall button ui is not ready yet. This method is called again once the incall button ui is diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java index 80ea75e99..2c956c8be 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java @@ -38,6 +38,7 @@ import java.lang.annotation.RetentionPolicy; 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 @@ public @interface InCallButtonIds { 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 5239d9d34..ee03c3d41 100644 --- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java +++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java @@ -58,6 +58,8 @@ public class InCallButtonIdsExtension { 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 28dd84c42..f22efeb2b 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); + void setCallRecordingState(boolean isRecording); + + void setCallRecordingDuration(long durationMs); + + 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 4cf40ef6a..4e25ff098 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(); + void callRecordClicked(boolean checked); + Context getContext(); } diff --git a/java/com/android/incallui/res/values/cm_strings.xml b/java/com/android/incallui/res/values/cm_strings.xml new file mode 100644 index 000000000..5e65ffc3b --- /dev/null +++ b/java/com/android/incallui/res/values/cm_strings.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2013-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. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <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="onscreenCallRecordingText">Recording call - <xliff:g id="duration" example="00:10">%1$s</xliff:g></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/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java index 649e80840..3e76f1f2b 100644 --- a/java/com/android/incallui/rtt/impl/RttChatFragment.java +++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java @@ -595,4 +595,13 @@ public class RttChatFragment extends Fragment @Override public void onAudioRouteSelectorDismiss() {} + + @Override + public void requestCallRecordingPermissions(String[] permissions) {} + + @Override + public void setCallRecordingDuration(long duration) {} + + @Override + public void setCallRecordingState(boolean isRecording) {} } diff --git a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java index 89db07903..07965b985 100644 --- a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java +++ b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java @@ -797,6 +797,18 @@ public class SurfaceViewVideoCallFragment extends Fragment } @Override + public void setCallRecordingState(boolean isRecording) { + } + + @Override + public void setCallRecordingDuration(long durationMs) { + } + + @Override + public void requestCallRecordingPermissions(String[] permissions) { + } + + @Override public void updateButtonStates() { LogUtil.i("SurfaceViewVideoCallFragment.updateButtonState", null); speakerButtonController.updateButtonState(); diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java index edfc17c46..3fbce5c76 100644 --- a/java/com/android/incallui/video/impl/VideoCallFragment.java +++ b/java/com/android/incallui/video/impl/VideoCallFragment.java @@ -909,6 +909,18 @@ public class VideoCallFragment extends Fragment } @Override + public void setCallRecordingState(boolean isRecording) { + } + + @Override + public void setCallRecordingDuration(long durationMs) { + } + + @Override + public void requestCallRecordingPermissions(String[] permissions) { + } + + @Override public void updateButtonStates() { LogUtil.i("VideoCallFragment.updateButtonState", null); speakerButtonController.updateButtonState(); |