summaryrefslogtreecommitdiff
path: root/java/com/android/incallui
diff options
context:
space:
mode:
authorDanny Baumann <dannybaumann@web.de>2018-07-09 11:19:24 +0200
committerMichael Bestas <mkbestas@lineageos.org>2020-12-12 01:23:35 +0200
commita6593be278cc0f94387289f99607a050efe7878b (patch)
tree4f12054f303f309215de552aabb97e2cfb9d7937 /java/com/android/incallui
parent399647dde74e8fbf152c98b36657944fd5369eea (diff)
Re-add call recording.
Author: Danny Baumann <dannybaumann@web.de> Date: Mon Jul 9 11:19:24 2018 +0200 Re-add call recording. Change-Id: I53fadf5754b5b6cc3e9920d57480e470e2305ac0 Author: Markus Gruber <gruberma@outlook.at> Date: Sat Oct 13 09:17:01 2018 +0200 Allow call recording for Austria * Call recording is legal in Austria, so it should be available in the UI Change-Id: Iaae0b222d2a1108572832732471e7e063f84dd1f Author: Alexandre Pary <alexandre.pary@gmail.com> Date: Wed Oct 17 11:33:35 2018 +0200 Allow call recording for Belgium * Call recording is legal in Belgium, so it should be available in the UI Change-Id: I0d18c5c31aa5fbde08a849932ac0c8088508dbd8 Author: Arekusu Rin <alexmatteotv@gmail.com> Date: Thu Oct 18 10:20:42 2018 +0200 Allow call recording for Bulgaria. * Call recording is legal, subject to certain restrictions, in Bulgaria. Call recording without notification or one side's consent is not a criminal offense, and only affects the admissibility of said call recording as evidence. Change-Id: Ie35f23056914fb2e7639ea509675e21e7fdfab26 (cherry picked from commit 4cec325c31dbe5894ab576b6161065ad0458612d) Author: Bruno Martins <bgcngm@gmail.com> Date: Tue Oct 23 21:03:47 2018 +0100 res: Fix malformed XML * The legal precedent source URL includes double dashes and breaks aapt2 compilation. Replace it by a shortened one. Change-Id: Ic1cb1b6af16d27649e36478ca7597b78b93b1338 Author: Arekusu Rin <alexmatteotv@gmail.com> Date: Thu Oct 25 12:50:57 2018 +0200 Enable or disable call recording for numerous countries via MCC. * This change handles call recording within the Dialer. Changes were made to the template of all of the XML files, and all links were changed to https, where possible. Quotes of the precedents and/or laws can be found within each country's XML file. Countries' whose status was not changed are not explicitly mentioned below, despite any changes to their files. * Call recording is disabled for: Andorra, Iceland, Indonesia, Monaco, Switzerland, the United States of America and some of its territories - Guam, Northern Mariana Islands, Puerto Rico and the United States Virgin Islands. * Call recording is enabled for: Albania, American Samoa, Argentina, Armenia, Aruba, Belarus, Bonaire, Bosnia and Herzegovina, Brazil, Canada, Chile, Croatia, Curaçao, Cyprus, Estonia, Faroe Islands, French Guiana, French Polynesia, Georgia, Greece, Greenland, Guadeloupe, Hungary, India, Ireland, Israel, Japan, Kosovo, Latvia, Liechtenstein, Lithuania, Luxembourg, Malta, Martinique, Mayotte, Moldova, Montenegro, Morocco, New Caledonia, New Zealand, North Macedonia, Peru, Russia, Réunion, Saba, Saint Barthélemy, Saint-Martin, Saint-Pierre-et-Miquelon, Serbia, Singapore, Sint Eustatius, Sint Maarten, Slovakia, Slovenia, South Africa, South Korea, Turkey, Ukraine and Wallis-et-Futuna. Change-Id: Iba5b7028d26cac281099f81bf3d5c21e2ee4d1a9 Author: Arekusu Rin <alexmatteotv@gmail.com> Date: Wed Jun 12 09:58:05 2019 +0200 Enable Call Recording for Sri Lanka and Costa Rica. * Call recording is enabled for: Sri Lanka (413) and Costa Rica (712). * Fixes: Removed newline from Belgium (206) and space from Russia (250). Change-Id: I4c9ecf41e9fd472b97fff5cd03800414737be87a Author: Danny Baumann <dannybaumann@web.de> Date: Thu Nov 7 08:34:44 2019 +0100 Base 'call recording allowed' decision on current country. Selection of resources by MCC happens via the SIM MCC, but what matters for legislation is the current country, not the country the SIM origins from. Because of that, move the decision about whether call recording is allowed or not to the current country instead of SIM MCC. Change-Id: I0ee365d7af8e3392716318e5a51e12e0efe7029a Author: Han Wang <416810799@qq.com> Date: Wed Nov 20 13:27:00 2019 +0200 Enable call recording for China Change-Id: Id51a2e6a119e99ff50696b50513aed323c61565c Author: mhkjahromi <m.h.k.jahromi@gmail.com> Date: Sat Dec 7 18:32:20 2019 +0330 Enable call recording for Iran Change-Id: I5640405d9bd38ac3d83fd618543190c1b0d800fb Author: Danny Baumann <dannybaumann@web.de> Date: Thu Feb 20 13:19:27 2020 +0100 Refactor call recording to use MediaProvider. Change-Id: Id53d43d8bf10715a1597ff754f6c38a992302190 Author: Danny Baumann <dannybaumann@web.de> Date: Fri Jun 5 13:19:46 2020 +0200 Iterate old recordings properly when migrating call recording data. SparseArray.get() expects a key, not an index. Change-Id: I0ba40180dc9df9f8a8f4036ccbe47cc59a50cfbb Change-Id: Ie9e0af8ccadb1bab1c52a5d905344d0c8fcab92c
Diffstat (limited to 'java/com/android/incallui')
-rw-r--r--java/com/android/incallui/CallButtonPresenter.java84
-rw-r--r--java/com/android/incallui/InCallServiceImpl.java2
-rw-r--r--java/com/android/incallui/call/CallList.java10
-rw-r--r--java/com/android/incallui/call/CallRecorder.java336
-rw-r--r--java/com/android/incallui/callpending/CallPendingActivity.java3
-rw-r--r--java/com/android/incallui/incall/impl/ButtonChooserFactory.java7
-rw-r--r--java/com/android/incallui/incall/impl/ButtonController.java91
-rw-r--r--java/com/android/incallui/incall/impl/CheckableLabeledButton.java4
-rw-r--r--java/com/android/incallui/incall/impl/InCallFragment.java40
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonIds.java4
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java2
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUi.java6
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java2
-rw-r--r--java/com/android/incallui/res/values/cm_strings.xml26
-rw-r--r--java/com/android/incallui/rtt/impl/RttChatFragment.java9
-rw-r--r--java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java12
-rw-r--r--java/com/android/incallui/video/impl/VideoCallFragment.java12
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();