summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk12
-rw-r--r--AndroidManifest.xml3
-rw-r--r--assets/quantum/res/drawable/quantum_ic_record_white_36.xml9
-rw-r--r--java/com/android/dialer/app/res/values/cm_arrays.xml29
-rw-r--r--java/com/android/dialer/app/res/values/cm_strings.xml7
-rw-r--r--java/com/android/dialer/app/res/values/colors.xml2
-rw-r--r--java/com/android/dialer/app/res/xml/file_paths.xml4
-rw-r--r--java/com/android/dialer/app/res/xml/sound_settings.xml14
-rw-r--r--java/com/android/dialer/app/settings/SoundSettingsFragment.java5
-rw-r--r--java/com/android/dialer/binary/common/DialerApplication.java5
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsActivity.java7
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsActivityCommon.java16
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsAdapter.java7
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java7
-rw-r--r--java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java77
-rw-r--r--java/com/android/dialer/calldetails/OldCallDetailsActivity.java7
-rw-r--r--java/com/android/dialer/calldetails/OldCallDetailsAdapter.java7
-rw-r--r--java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml26
-rw-r--r--java/com/android/dialer/calldetails/res/layout/call_details_entry.xml23
-rw-r--r--java/com/android/dialer/calldetails/res/values/cm_strings.xml23
-rw-r--r--java/com/android/dialer/callrecord/AndroidManifest.xml26
-rw-r--r--java/com/android/dialer/callrecord/CallRecording.aidl3
-rw-r--r--java/com/android/dialer/callrecord/CallRecording.java107
-rw-r--r--java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java157
-rw-r--r--java/com/android/dialer/callrecord/CallRecordingDataStore.java224
-rw-r--r--java/com/android/dialer/callrecord/ICallRecorderService.aidl37
-rw-r--r--java/com/android/dialer/callrecord/impl/CallRecorderService.java222
-rw-r--r--java/com/android/dialer/callrecord/res/values/config.xml23
-rw-r--r--java/com/android/dialer/callrecord/res/xml/call_record_states.xml1324
-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
-rw-r--r--privapp_whitelist_com.android.dialer-ext.xml22
47 files changed, 3064 insertions, 21 deletions
diff --git a/Android.mk b/Android.mk
index b7a4a8ffe..db6cff7bd 100644
--- a/Android.mk
+++ b/Android.mk
@@ -79,6 +79,8 @@ LOCAL_SRC_FILES += $(call all-proto-files-under, $(BASE_DIR))
LOCAL_SRC_FILES += $(call all-Iaidl-files-under, $(BASE_DIR))
LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
+LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/java
+
LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)
LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(RES_DIRS))
@@ -176,9 +178,19 @@ LOCAL_PRIVILEGED_MODULE := true
LOCAL_PRODUCT_MODULE := true
LOCAL_USE_AAPT2 := true
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.dialer
+LOCAL_REQUIRED_MODULES += privapp_whitelist_com.android.dialer-ext.xml
include $(BUILD_PACKAGE)
+include $(CLEAR_VARS)
+LOCAL_MODULE := privapp_whitelist_com.android.dialer-ext.xml
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT_ETC)/permissions
+LOCAL_PRODUCT_MODULE := true
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+include $(BUILD_PREBUILT)
+
# Cleanup local state
BASE_DIR :=
EXCLUDE_FILES :=
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d96d27629..9ec517382 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -114,7 +114,8 @@
android:appCategory="social"
android:supportsRtl="true"
android:usesCleartextTraffic="false"
- android:extractNativeLibs="false">
+ android:extractNativeLibs="false"
+ android:requestLegacyExternalStorage="true">
</application>
</manifest>
diff --git a/assets/quantum/res/drawable/quantum_ic_record_white_36.xml b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
new file mode 100644
index 000000000..35aaa4134
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="36dp"
+ android:width="36dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,12C19,15.86 15.86,19 12,19C8.14,19 5,15.86 5,12C5,8.14 8.14,5 12,5C15.86,5 19,8.14 19,12Z" />
+</vector>
diff --git a/java/com/android/dialer/app/res/values/cm_arrays.xml b/java/com/android/dialer/app/res/values/cm_arrays.xml
new file mode 100644
index 000000000..a788fd342
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_arrays.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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-array name="call_recording_encoder_entries" translatable="false">
+ <item>@string/wb_amr_format</item>
+ <item>@string/aac_format</item>
+ </string-array>
+
+ <string-array name="call_recording_encoder_values" translatable="false">
+ <item>"0"</item>
+ <item>"1"</item>
+ </string-array>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_strings.xml b/java/com/android/dialer/app/res/values/cm_strings.xml
index 0ba0d500a..b28dcaeb2 100644
--- a/java/com/android/dialer/app/res/values/cm_strings.xml
+++ b/java/com/android/dialer/app/res/values/cm_strings.xml
@@ -31,4 +31,11 @@
<string name="incall_dnd_dialog_message">In order to enable Do Not Disturb, the Phone app needs to be granted the permission to control the Do Not Disturb status.\nPlease allow it.</string>
<string name="allow">Allow</string>
<string name="deny">Deny</string>
+
+ <string name="call_recording_category_key" translatable="false">call_recording_category</string>
+ <string name="call_recording_category_title">Call recording</string>
+ <string name="call_recording_format_key" translatable="false">call_recording_format</string>
+ <string name="call_recording_format">Audio format</string>
+ <string name="wb_amr_format" translatable="false">AMR-WB</string>
+ <string name="aac_format" translatable="false">AAC</string>
</resources>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
index a9713474d..16fb2d098 100644
--- a/java/com/android/dialer/app/res/values/colors.xml
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -18,6 +18,8 @@
<color name="voicemail_icon_disabled_tint">#80000000</color>
<color name="voicemail_playpause_icon_tint">?colorIcon</color>
+ <color name="call_record_playback_icon_color">#8a000000</color>
+
<!-- Text color for the "Remove" text when a contact is dragged on top of the remove view -->
<color name="remove_highlighted_text_color">#FF3F3B</color>
diff --git a/java/com/android/dialer/app/res/xml/file_paths.xml b/java/com/android/dialer/app/res/xml/file_paths.xml
index 0dd41a085..b43f45093 100644
--- a/java/com/android/dialer/app/res/xml/file_paths.xml
+++ b/java/com/android/dialer/app/res/xml/file_paths.xml
@@ -22,4 +22,8 @@
<files-path
name="voicemails"
path="voicemails/"/>
+ <!-- Offer access to saved call recordings -->
+ <external-path
+ name="recordings"
+ path="CallRecordings/"/>
</paths>
diff --git a/java/com/android/dialer/app/res/xml/sound_settings.xml b/java/com/android/dialer/app/res/xml/sound_settings.xml
index 4da5c1514..aa025874f 100644
--- a/java/com/android/dialer/app/res/xml/sound_settings.xml
+++ b/java/com/android/dialer/app/res/xml/sound_settings.xml
@@ -71,4 +71,18 @@
</PreferenceCategory>
+ <PreferenceCategory
+ android:key="@string/call_recording_category_key"
+ android:title="@string/call_recording_category_title">
+
+ <ListPreference
+ android:key="@string/call_recording_format_key"
+ android:title="@string/call_recording_format"
+ android:summary="%s"
+ android:entries="@array/call_recording_encoder_entries"
+ android:entryValues="@array/call_recording_encoder_values"
+ android:defaultValue="0" />
+
+ </PreferenceCategory>
+
</PreferenceScreen>
diff --git a/java/com/android/dialer/app/settings/SoundSettingsFragment.java b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
index d9f24ab7e..f7fc0d0b6 100644
--- a/java/com/android/dialer/app/settings/SoundSettingsFragment.java
+++ b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
@@ -37,6 +37,7 @@ import android.telephony.CarrierConfigManager;
import android.telephony.TelephonyManager;
import android.widget.Toast;
import com.android.dialer.app.R;
+import com.android.dialer.callrecord.impl.CallRecorderService;
import com.android.dialer.util.SettingsUtil;
public class SoundSettingsFragment extends PreferenceFragment
@@ -140,6 +141,10 @@ public class SoundSettingsFragment extends PreferenceFragment
getPreferenceScreen().removePreference(dtmfToneLength);
dtmfToneLength = null;
}
+ if (!CallRecorderService.isEnabled(getActivity())) {
+ getPreferenceScreen().removePreference(
+ findPreference(context.getString(R.string.call_recording_category_key)));
+ }
notificationManager = context.getSystemService(NotificationManager.class);
}
diff --git a/java/com/android/dialer/binary/common/DialerApplication.java b/java/com/android/dialer/binary/common/DialerApplication.java
index 31d4d828e..0f150253a 100644
--- a/java/com/android/dialer/binary/common/DialerApplication.java
+++ b/java/com/android/dialer/binary/common/DialerApplication.java
@@ -26,6 +26,7 @@ import com.android.dialer.calllog.CallLogComponent;
import com.android.dialer.calllog.CallLogFramework;
import com.android.dialer.calllog.config.CallLogConfig;
import com.android.dialer.calllog.config.CallLogConfigComponent;
+import com.android.dialer.callrecord.CallRecordingAutoMigrator;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.inject.HasRootComponent;
@@ -48,6 +49,10 @@ public abstract class DialerApplication extends Application implements HasRootCo
new FilteredNumberAsyncQueryHandler(this),
DialerExecutorComponent.get(this).dialerExecutorFactory())
.asyncAutoMigrate();
+ new CallRecordingAutoMigrator(
+ this.getApplicationContext(),
+ DialerExecutorComponent.get(this).dialerExecutorFactory())
+ .asyncAutoMigrate();
initializeAnnotatedCallLog();
PersistentLogger.initialize(this);
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index 36b830851..c1acbc373 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -28,6 +28,7 @@ import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDeta
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.ReportCallIdListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.protos.ProtoParsers;
@@ -93,7 +94,8 @@ public final class CallDetailsActivity extends CallDetailsActivityCommon {
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
return new CallDetailsAdapter(
this,
headerInfo,
@@ -101,7 +103,8 @@ public final class CallDetailsActivity extends CallDetailsActivityCommon {
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
}
@Override
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
index a26f322dd..79e761368 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
@@ -38,6 +38,7 @@ import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity;
import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutor.FailureListener;
@@ -96,6 +97,7 @@ abstract class CallDetailsActivityCommon extends AppCompatActivity {
private CallDetailsAdapterCommon adapter;
private CallDetailsEntries callDetailsEntries;
private UiListener<ImmutableSet<String>> checkRttTranscriptAvailabilityListener;
+ private CallRecordingDataStore callRecordingDataStore;
/**
* Handles the intent that launches {@link OldCallDetailsActivity} or {@link CallDetailsActivity},
@@ -108,7 +110,8 @@ abstract class CallDetailsActivityCommon extends AppCompatActivity {
CallDetailsEntryViewHolder.CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener,
CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
- CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener);
+ CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore);
/** Returns the phone number of the call details. */
protected abstract String getNumber();
@@ -129,12 +132,20 @@ abstract class CallDetailsActivityCommon extends AppCompatActivity {
checkRttTranscriptAvailabilityListener =
DialerExecutorComponent.get(this)
.createUiListener(getFragmentManager(), "Query RTT transcript availability");
+ callRecordingDataStore = new CallRecordingDataStore();
handleIntent(getIntent());
setupRecyclerViewForEntries();
}
@Override
@CallSuper
+ protected void onDestroy() {
+ super.onDestroy();
+ callRecordingDataStore.close();
+ }
+
+ @Override
+ @CallSuper
protected void onResume() {
super.onResume();
@@ -205,7 +216,8 @@ abstract class CallDetailsActivityCommon extends AppCompatActivity {
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
index 40d856fa7..7e5ebe170 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -23,6 +23,7 @@ import android.view.View;
import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntryListener;
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.glidephotomanager.PhotoInfo;
/**
@@ -43,14 +44,16 @@ final class CallDetailsAdapter extends CallDetailsAdapterCommon {
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
super(
context,
callDetailsEntries,
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
this.headerInfo = calldetailsHeaderInfo;
}
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java b/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
index ec9263f1f..d33fea816 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
@@ -32,6 +32,7 @@ import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHea
import com.android.dialer.calllogutils.CallTypeHelper;
import com.android.dialer.calllogutils.CallbackActionHelper;
import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.duo.DuoComponent;
import com.android.dialer.glidephotomanager.PhotoInfo;
@@ -51,6 +52,7 @@ abstract class CallDetailsAdapterCommon extends RecyclerView.Adapter<RecyclerVie
private final ReportCallIdListener reportCallIdListener;
private final DeleteCallDetailsListener deleteCallDetailsListener;
private final CallTypeHelper callTypeHelper;
+ private final CallRecordingDataStore callRecordingDataStore;
private CallDetailsEntries callDetailsEntries;
@@ -75,12 +77,14 @@ abstract class CallDetailsAdapterCommon extends RecyclerView.Adapter<RecyclerVie
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
this.callDetailsEntries = callDetailsEntries;
this.callDetailsEntryListener = callDetailsEntryListener;
this.callDetailsHeaderListener = callDetailsHeaderListener;
this.reportCallIdListener = reportCallIdListener;
this.deleteCallDetailsListener = deleteCallDetailsListener;
+ this.callRecordingDataStore = callRecordingDataStore;
this.callTypeHelper =
new CallTypeHelper(context.getResources(), DuoComponent.get(context).getDuo());
}
@@ -123,6 +127,7 @@ abstract class CallDetailsAdapterCommon extends RecyclerView.Adapter<RecyclerVie
getPhotoInfo(),
entry,
callTypeHelper,
+ callRecordingDataStore,
!entry.getHistoryResultsList().isEmpty() && position != getItemCount() - 2);
}
}
diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
index 05957ae80..a9be544a0 100644
--- a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
@@ -16,23 +16,36 @@
package com.android.dialer.calldetails;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.provider.CallLog.Calls;
+import android.provider.MediaStore;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
+import android.support.v4.content.FileProvider;
import android.support.v4.os.BuildCompat;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.view.Menu;
import android.view.View;
+import android.webkit.MimeTypeMap;
import android.widget.ImageView;
+import android.widget.PopupMenu;
import android.widget.TextView;
+import android.widget.Toast;
import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
import com.android.dialer.calllogutils.CallLogDates;
import com.android.dialer.calllogutils.CallLogDurations;
import com.android.dialer.calllogutils.CallTypeHelper;
import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.CallRecordingDataStore;
+import com.android.dialer.callrecord.impl.CallRecorderService;
import com.android.dialer.common.LogUtil;
import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult;
import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult.Type;
@@ -41,6 +54,11 @@ import com.android.dialer.oem.MotorolaUtils;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.IntentUtil;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
/** ViewHolder for call entries in {@link OldCallDetailsActivity} or {@link CallDetailsActivity}. */
public class CallDetailsEntryViewHolder extends ViewHolder {
@@ -66,6 +84,7 @@ public class CallDetailsEntryViewHolder extends ViewHolder {
private final TextView rttTranscript;
private final ImageView multimediaImage;
+ private final TextView playbackButton;
// TODO(maxwelb): Display this when location is stored - a bug
@SuppressWarnings("unused")
@@ -83,6 +102,7 @@ public class CallDetailsEntryViewHolder extends ViewHolder {
callTime = (TextView) container.findViewById(R.id.call_time);
callDuration = (TextView) container.findViewById(R.id.call_duration);
+ playbackButton = (TextView) container.findViewById(R.id.play_recordings);
multimediaImageContainer = container.findViewById(R.id.multimedia_image_container);
multimediaDetailsContainer = container.findViewById(R.id.ec_container);
multimediaDivider = container.findViewById(R.id.divider);
@@ -101,6 +121,7 @@ public class CallDetailsEntryViewHolder extends ViewHolder {
PhotoInfo photoInfo,
CallDetailsEntry entry,
CallTypeHelper callTypeHelper,
+ CallRecordingDataStore callRecordingDataStore,
boolean showMultimediaDivider) {
int callType = entry.getCallType();
boolean isVideoCall = (entry.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO;
@@ -139,6 +160,22 @@ public class CallDetailsEntryViewHolder extends ViewHolder {
CallLogDurations.formatDurationAndDataUsageA11y(
context, entry.getDuration(), entry.getDataUsage()));
}
+
+ // do this synchronously to prevent recordings from "popping in" after detail item is displayed
+ final List<CallRecording> recordings;
+ if (CallRecorderService.isEnabled(context)) {
+ callRecordingDataStore.open(context); // opens unless already open
+ recordings = callRecordingDataStore.getRecordings(number, entry.getDate());
+ } else {
+ recordings = null;
+ }
+
+ int count = recordings != null ? recordings.size() : 0;
+ playbackButton.setOnClickListener(v -> handleRecordingClick(v, recordings));
+ playbackButton.setText(
+ context.getResources().getQuantityString(R.plurals.play_recordings, count, count));
+ playbackButton.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
+
setMultimediaDetails(number, entry, showMultimediaDivider);
if (isRttCall) {
if (entry.getHasRttTranscript()) {
@@ -209,6 +246,46 @@ public class CallDetailsEntryViewHolder extends ViewHolder {
DialerUtils.startActivityWithErrorToast(context, IntentUtil.getSendSmsIntent(number));
}
+ private void handleRecordingClick(View v, List<CallRecording> recordings) {
+ final Context context = v.getContext();
+ if (recordings.size() == 1) {
+ playRecording(context, recordings.get(0));
+ } else {
+ PopupMenu menu = new PopupMenu(context, v);
+ String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ DateFormat.is24HourFormat(context) ? "Hmss" : "hmssa");
+ SimpleDateFormat format = new SimpleDateFormat(pattern);
+
+ for (int i = 0; i < recordings.size(); i++) {
+ final long startTime = recordings.get(i).startRecordingTime;
+ final String formattedDate = format.format(new Date(startTime));
+ menu.getMenu().add(Menu.NONE, i, i, formattedDate);
+ }
+ menu.setOnMenuItemClickListener(item -> {
+ playRecording(context, recordings.get(item.getItemId()));
+ return true;
+ });
+ menu.show();
+ }
+ }
+
+ private void playRecording(Context context, CallRecording recording) {
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, recording.mediaId);
+ String extension = MimeTypeMap.getFileExtensionFromUrl(recording.fileName);
+ String mime = !TextUtils.isEmpty(extension)
+ ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, mime)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, R.string.call_playback_no_app_found_toast, Toast.LENGTH_LONG)
+ .show();
+ }
+ }
+
private static boolean isIncoming(@NonNull HistoryResult historyResult) {
return historyResult.getType() == Type.INCOMING_POST_CALL
|| historyResult.getType() == Type.INCOMING_CALL_COMPOSER;
diff --git a/java/com/android/dialer/calldetails/OldCallDetailsActivity.java b/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
index 26217ab8a..0f53d6908 100644
--- a/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
@@ -22,6 +22,7 @@ import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntr
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.ReportCallIdListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.dialercontact.DialerContact;
import com.android.dialer.protos.ProtoParsers;
@@ -80,7 +81,8 @@ public final class OldCallDetailsActivity extends CallDetailsActivityCommon {
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
return new OldCallDetailsAdapter(
/* context = */ this,
contact,
@@ -88,7 +90,8 @@ public final class OldCallDetailsActivity extends CallDetailsActivityCommon {
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
}
@Override
diff --git a/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java b/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
index 878803cc3..af54538db 100644
--- a/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
@@ -23,6 +23,7 @@ import android.view.View;
import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntryListener;
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.dialercontact.DialerContact;
import com.android.dialer.glidephotomanager.PhotoInfo;
import com.android.dialer.lettertile.LetterTileDrawable;
@@ -45,14 +46,16 @@ final class OldCallDetailsAdapter extends CallDetailsAdapterCommon {
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
super(
context,
callDetailsEntries,
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
this.contact = contact;
}
diff --git a/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml b/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml
new file mode 100644
index 000000000..c6fb87f74
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+
+ <path
+ android:fillColor="@color/call_record_playback_icon_color"
+ android:pathData="M 21,30.75 L 30,24 21,17.25 21,30.75 Z M 24,9 C 15.7125,9 9,15.7125 9,24 9,32.2875 15.7125,39 24,39 32.2875,39 39,32.2875 39,24 39,15.7125 32.2875,9 24,9 Z m 0,27 c -6.615,0 -12,-5.385 -12,-12 0,-6.615 5.385,-12 12,-12 6.615,0 12,5.385 12,12 0,6.615 -5.385,12 -12,12 z" />
+</vector>
+
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
index bfbb4f8a9..ffe3ade5e 100644
--- a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
@@ -19,7 +19,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingTop="@dimen/call_entry_padding">
+ android:paddingTop="@dimen/call_entry_padding"
+ android:paddingBottom="@dimen/call_entry_bottom_padding">
<com.android.dialer.calllogutils.CallTypeIconsView
android:id="@+id/call_direction"
@@ -43,7 +44,6 @@
style="@style/Dialer.TextAppearance.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/call_entry_bottom_padding"
android:layout_marginStart="@dimen/call_entry_text_left_margin"
android:layout_marginEnd="16dp"
android:layout_below="@+id/call_type"/>
@@ -56,12 +56,27 @@
android:layout_marginEnd="@dimen/call_entry_padding"
android:layout_alignParentEnd="true"/>
+ <TextView
+ android:id="@+id/play_recordings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/call_time"
+ android:paddingStart="@dimen/call_entry_text_left_margin"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:gravity="center_vertical"
+ android:drawableStart="@drawable/recording_playback_button"
+ android:drawablePadding="4dp"
+ android:background="?attr/selectableItemBackground"
+ android:visibility="gone"
+ style="@style/Dialer.TextAppearance.Secondary"/>
+
<include
android:id="@+id/ec_container"
layout="@layout/ec_data_container"
android:layout_width="match_parent"
android:layout_height="@dimen/ec_container_height"
- android:layout_below="@+id/call_time"
+ android:layout_below="@id/play_recordings"
android:visibility="gone"/>
<TextView
@@ -97,4 +112,4 @@
android:layout_below="@id/rtt_transcript"
android:background="@color/dialer_divider_line_color"
android:visibility="gone"/>
-</RelativeLayout> \ No newline at end of file
+</RelativeLayout>
diff --git a/java/com/android/dialer/calldetails/res/values/cm_strings.xml b/java/com/android/dialer/calldetails/res/values/cm_strings.xml
new file mode 100644
index 000000000..076a49479
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/cm_strings.xml
@@ -0,0 +1,23 @@
+<?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">
+ <plurals name="play_recordings">
+ <item quantity="one">Play recording</item>
+ <item quantity="other">Play recordings</item>
+ </plurals>
+ <string name="call_playback_no_app_found_toast">No app could be found for playback of the selected recording.</string>
+</resources>
diff --git a/java/com/android/dialer/callrecord/AndroidManifest.xml b/java/com/android/dialer/callrecord/AndroidManifest.xml
new file mode 100644
index 000000000..5e25c7351
--- /dev/null
+++ b/java/com/android/dialer/callrecord/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!-- Copyright (C) 2018 The LineageOS 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer">
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
+
+ <application>
+ <service android:name="com.android.dialer.callrecord.impl.CallRecorderService"
+ android:process="com.android.incallui" />
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/callrecord/CallRecording.aidl b/java/com/android/dialer/callrecord/CallRecording.aidl
new file mode 100644
index 000000000..e8d65bef2
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.aidl
@@ -0,0 +1,3 @@
+package com.android.dialer.callrecord;
+
+parcelable CallRecording;
diff --git a/java/com/android/dialer/callrecord/CallRecording.java b/java/com/android/dialer/callrecord/CallRecording.java
new file mode 100644
index 000000000..a887d1a56
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callrecord;
+
+import android.content.ContentValues;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public final class CallRecording implements Parcelable {
+ public String phoneNumber;
+ public long creationTime;
+ public String fileName;
+ public long startRecordingTime;
+ public long mediaId;
+
+ public static final Parcelable.Creator<CallRecording> CREATOR =
+ new Parcelable.Creator<CallRecording>() {
+ @Override
+ public CallRecording createFromParcel(Parcel in) {
+ return new CallRecording(in);
+ }
+
+ @Override
+ public CallRecording[] newArray(int size) {
+ return new CallRecording[size];
+ }
+ };
+
+ public CallRecording(String phoneNumber, long creationTime,
+ String fileName, long startRecordingTime, long mediaId) {
+ this.phoneNumber = phoneNumber;
+ this.creationTime = creationTime;
+ this.fileName = fileName;
+ this.startRecordingTime = startRecordingTime;
+ this.mediaId = mediaId;
+ }
+
+ public CallRecording(Parcel in) {
+ phoneNumber = in.readString();
+ creationTime = in.readLong();
+ fileName = in.readString();
+ startRecordingTime = in.readLong();
+ mediaId = in.readLong();
+ }
+
+ public static ContentValues generateMediaInsertValues(String fileName, long creationTime) {
+ final ContentValues cv = new ContentValues(5);
+
+ cv.put(MediaStore.Audio.Media.RELATIVE_PATH, "Music/Call Recordings");
+ cv.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);
+ cv.put(MediaStore.Audio.Media.DATE_TAKEN, creationTime);
+ cv.put(MediaStore.Audio.Media.IS_PENDING, 1);
+
+ final String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+ final String mime = !TextUtils.isEmpty(extension)
+ ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+ cv.put(MediaStore.Audio.Media.MIME_TYPE, mime);
+
+ return cv;
+ }
+
+ public static ContentValues generateCompletedValues() {
+ final ContentValues cv = new ContentValues(1);
+ cv.put(MediaStore.Audio.Media.IS_PENDING, 0);
+ return cv;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(phoneNumber);
+ out.writeLong(creationTime);
+ out.writeString(fileName);
+ out.writeLong(startRecordingTime);
+ out.writeLong(mediaId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "phoneNumber=" + phoneNumber + ", creationTime=" + creationTime +
+ ", fileName=" + fileName + ", startRecordingTime=" + startRecordingTime;
+ }
+}
diff --git a/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java b/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java
new file mode 100644
index 000000000..81c16124f
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callrecord;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorFactory;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+
+public class CallRecordingAutoMigrator {
+ private static final String TAG = "CallRecordingAutoMigrator";
+
+ @NonNull
+ private final Context appContext;
+ @NonNull private final DialerExecutorFactory dialerExecutorFactory;
+
+ public CallRecordingAutoMigrator(
+ @NonNull Context appContext,
+ @NonNull DialerExecutorFactory dialerExecutorFactory) {
+ this.appContext = Assert.isNotNull(appContext);
+ this.dialerExecutorFactory = Assert.isNotNull(dialerExecutorFactory);
+ }
+
+ public void asyncAutoMigrate() {
+ dialerExecutorFactory
+ .createNonUiTaskBuilder(new ShouldAttemptAutoMigrate(appContext))
+ .onSuccess(this::autoMigrate)
+ .build()
+ .executeParallel(null);
+ }
+
+ @TargetApi(26)
+ private void autoMigrate(boolean shouldAttemptAutoMigrate) {
+ if (!shouldAttemptAutoMigrate) {
+ return;
+ }
+
+ final CallRecordingDataStore store = new CallRecordingDataStore();
+ store.open(appContext);
+
+ final ContentResolver cr = appContext.getContentResolver();
+ final SparseArray<CallRecording> oldRecordingData = store.getUnmigratedRecordingData();
+ final File dir = Environment.getExternalStoragePublicDirectory("CallRecordings");
+ for (File recording : dir.listFiles()) {
+ OutputStream os = null;
+ try {
+ // determine data store ID and call creation time of recording
+ int id = -1;
+ long creationTime = System.currentTimeMillis();
+ for (int i = 0; i < oldRecordingData.size(); i++) {
+ if (TextUtils.equals(recording.getName(), oldRecordingData.valueAt(i).fileName)) {
+ creationTime = oldRecordingData.valueAt(i).creationTime;
+ id = oldRecordingData.keyAt(i);
+ break;
+ }
+ }
+
+ // create media store entry for recording
+ Uri uri = cr.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ CallRecording.generateMediaInsertValues(recording.getName(), creationTime));
+ os = cr.openOutputStream(uri);
+
+ // copy file contents to media store stream
+ Files.copy(recording.toPath(), os);
+
+ // insert media store id to store
+ if (id >= 0) {
+ store.updateMigratedRecording(id, Integer.parseInt(uri.getLastPathSegment()));
+ }
+
+ // mark recording as complete
+ cr.update(uri, CallRecording.generateCompletedValues(), null, null);
+
+ // delete file
+ LogUtils.i(TAG, "Successfully migrated recording " + recording + " (ID " + id + ")");
+ recording.delete();
+ } catch (IOException e) {
+ LogUtils.w(TAG, "Failed migrating call recording " + recording, e);
+ } finally {
+ if (os != null) {
+ IOUtils.closeQuietly(os);
+ }
+ }
+ }
+
+ if (dir.listFiles().length == 0) {
+ dir.delete();
+ }
+
+ store.close();
+ }
+
+ private static class ShouldAttemptAutoMigrate implements Worker<Void, Boolean> {
+ private final Context appContext;
+
+ ShouldAttemptAutoMigrate(Context appContext) {
+ this.appContext = appContext;
+ }
+
+ @Nullable
+ @Override
+ public Boolean doInBackground(@Nullable Void input) {
+ if (Build.VERSION.SDK_INT < 26) {
+ return false;
+ }
+ if (appContext.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i(TAG, "not attempting auto-migrate: no storage permission");
+ return false;
+ }
+
+ final File dir = Environment.getExternalStoragePublicDirectory("CallRecordings");
+ if (!dir.exists()) {
+ LogUtil.i(TAG, "not attempting auto-migrate: no recordings present");
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/java/com/android/dialer/callrecord/CallRecordingDataStore.java b/java/com/android/dialer/callrecord/CallRecordingDataStore.java
new file mode 100644
index 000000000..88b603b54
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecordingDataStore.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callrecord;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Persistent data store for call recordings. Usage:
+ * open()
+ * read/write operations
+ * close()
+ */
+public class CallRecordingDataStore {
+ private static final String TAG = "CallRecordingStore";
+ private SQLiteOpenHelper mOpenHelper = null;
+ private SQLiteDatabase mDatabase = null;
+
+ /**
+ * Open before reading/writing. Will not open handle if one is already open.
+ */
+ public void open(Context context) {
+ if (mDatabase == null) {
+ mOpenHelper = new CallRecordingSQLiteOpenHelper(context);
+ mDatabase = mOpenHelper.getWritableDatabase();
+ }
+ }
+
+ /**
+ * close when finished reading/writing
+ */
+ public void close() {
+ if (mDatabase != null) {
+ mDatabase.close();
+ }
+ if (mOpenHelper != null) {
+ mOpenHelper.close();
+ }
+ mDatabase = null;
+ mOpenHelper = null;
+ }
+
+ /**
+ * Save a recording in the data store
+ *
+ * @param recording the recording to store
+ */
+ public void putRecording(CallRecording recording) {
+ final String insertSql = "INSERT INTO " +
+ CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + ") " +
+ " VALUES (?, ?, ?, ?, ?)";
+
+ try {
+ SQLiteStatement stmt = mDatabase.compileStatement(insertSql);
+ int idx = 1;
+ stmt.bindString(idx++, recording.phoneNumber);
+ stmt.bindLong(idx++, recording.creationTime);
+ stmt.bindString(idx++, recording.fileName);
+ stmt.bindLong(idx++, System.currentTimeMillis());
+ stmt.bindLong(idx++, recording.mediaId);
+ long id = stmt.executeInsert();
+ Log.i(TAG, "Saved recording " + recording + " with id " + id);
+ } catch (SQLiteException e) {
+ Log.w(TAG, "Failed to save recording " + recording, e);
+ }
+ }
+
+ /**
+ * Get all recordings associated with a phone call
+ *
+ * @param phoneNumber phone number no spaces
+ * @param callCreationDate time that the call was created
+ * @return list of recordings
+ */
+ public List<CallRecording> getRecordings(String phoneNumber, long callCreationDate) {
+ List<CallRecording> resultList = new ArrayList<CallRecording>();
+
+ final String query = "SELECT " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID +
+ " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME +
+ " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " = ?" +
+ " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " = ?" +
+ " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " != 0" +
+ " ORDER BY " + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE;
+
+ String args[] = {
+ phoneNumber, String.valueOf(callCreationDate)
+ };
+
+ try {
+ Cursor cursor = mDatabase.rawQuery(query, args);
+ while (cursor.moveToNext()) {
+ String fileName = cursor.getString(0);
+ long creationDate = cursor.getLong(1);
+ long mediaId = cursor.getLong(2);
+ // FIXME: need to check whether media entry still exists?
+ resultList.add(
+ new CallRecording(phoneNumber, callCreationDate, fileName, creationDate, mediaId));
+ }
+ cursor.close();
+ } catch (SQLiteException e) {
+ Log.w(TAG, "Failed to fetch recordings for number " + phoneNumber +
+ ", date " + callCreationDate, e);
+ }
+
+ return resultList;
+ }
+
+ public SparseArray<CallRecording> getUnmigratedRecordingData() {
+ final String query = "SELECT " +
+ CallRecordingsContract.CallRecording._ID + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE +
+ " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME +
+ " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " == 0";
+ final SparseArray<CallRecording> result = new SparseArray<>();
+
+ try {
+ Cursor cursor = mDatabase.rawQuery(query, null);
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(0);
+ String phoneNumber = cursor.getString(1);
+ String fileName = cursor.getString(2);
+ long creationDate = cursor.getLong(3);
+ CallRecording recording = new CallRecording(
+ phoneNumber, creationDate, fileName, creationDate, 0);
+ result.put(id, recording);
+ }
+ cursor.close();
+ } catch (SQLiteException e) {
+ Log.w(TAG, "Failed to fetch recordings for migration", e);
+ }
+
+ return result;
+ }
+
+ public void updateMigratedRecording(int id, int mediaId) {
+ ContentValues cv = new ContentValues(1);
+ cv.put(CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID, mediaId);
+ mDatabase.update(CallRecordingsContract.CallRecording.TABLE_NAME, cv,
+ CallRecordingsContract.CallRecording._ID + " = ?", new String[] { String.valueOf(id) });
+ }
+
+ static class CallRecordingsContract {
+ static interface CallRecording extends BaseColumns {
+ static final String TABLE_NAME = "call_recordings";
+ static final String COLUMN_NAME_PHONE_NUMBER = "phone_number";
+ static final String COLUMN_NAME_CALL_DATE = "call_date";
+ static final String COLUMN_NAME_RECORDING_FILENAME = "recording_filename";
+ static final String COLUMN_NAME_CREATION_DATE = "creation_date";
+ static final String COLUMN_NAME_MEDIA_ID = "media_id";
+ }
+ }
+
+ static class CallRecordingSQLiteOpenHelper extends SQLiteOpenHelper {
+ private static final int VERSION = 2;
+ private static final String DB_NAME = "callrecordings.db";
+
+ public CallRecordingSQLiteOpenHelper(Context context) {
+ super(context, DB_NAME, null, VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+ CallRecordingsContract.CallRecording._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " TEXT," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " LONG," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + " TEXT, " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + " LONG," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " INTEGER DEFAULT 0" +
+ ");"
+ );
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS phone_number_call_date_index ON " +
+ CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ");"
+ );
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 2) {
+ db.execSQL("ALTER TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME +
+ " ADD COLUMN " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID +
+ " INTEGER DEFAULT 0;");
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/callrecord/ICallRecorderService.aidl b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
new file mode 100644
index 000000000..acbd5f8bb
--- /dev/null
+++ b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
@@ -0,0 +1,37 @@
+package com.android.dialer.callrecord;
+
+import com.android.dialer.callrecord.CallRecording;
+
+/**
+ * Service for recording phone calls. Only one recording may be active at a time
+ * (i.e. every call to startRecording should be followed by a call to stopRecording).
+ */
+interface ICallRecorderService {
+ /**
+ * Start a recording.
+ *
+ * @return true if recording started successfully
+ */
+ boolean startRecording(String phoneNumber, long creationTime);
+
+ /**
+ * stops the current recording
+ *
+ * @return call recording data including the output filename
+ */
+ CallRecording stopRecording();
+
+ /**
+ * Recording status
+ *
+ * @return true if there is an active recording
+ */
+ boolean isRecording();
+
+ /**
+ * Get recording currently in progress
+ *
+ * @return call recording object
+ */
+ CallRecording getActiveRecording();
+}
diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderService.java b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
new file mode 100644
index 000000000..298e8ade0
--- /dev/null
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.callrecord.impl;
+
+import android.app.Service;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import com.android.dialer.R;
+
+public class CallRecorderService extends Service {
+ private static final String TAG = "CallRecorderService";
+ private static final boolean DBG = false;
+
+ private MediaRecorder mMediaRecorder = null;
+ private CallRecording mCurrentRecording = null;
+
+ private SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd_HHmmssSSS");
+
+ private final ICallRecorderService.Stub mBinder = new ICallRecorderService.Stub() {
+ @Override
+ public CallRecording stopRecording() {
+ return stopRecordingInternal();
+ }
+
+ @Override
+ public boolean startRecording(String phoneNumber, long creationTime) throws RemoteException {
+ return startRecordingInternal(phoneNumber, creationTime);
+ }
+
+ @Override
+ public boolean isRecording() throws RemoteException {
+ return mMediaRecorder != null;
+ }
+
+ @Override
+ public CallRecording getActiveRecording() throws RemoteException {
+ return mCurrentRecording;
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ if (DBG) Log.d(TAG, "Creating CallRecorderService");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private int getAudioSource() {
+ return getResources().getInteger(R.integer.call_recording_audio_source);
+ }
+
+ private int getAudioFormatChoice() {
+ // This replicates PreferenceManager.getDefaultSharedPreferences, except
+ // that we need multi process preferences, as the pref is written in a separate
+ // process (com.android.dialer vs. com.android.incallui)
+ final String prefName = getPackageName() + "_preferences";
+ final SharedPreferences prefs = getSharedPreferences(prefName, MODE_MULTI_PROCESS);
+
+ try {
+ String value = prefs.getString(getString(R.string.call_recording_format_key), null);
+ if (value != null) {
+ return Integer.parseInt(value);
+ }
+ } catch (NumberFormatException e) {
+ // ignore and fall through
+ }
+ return 0;
+ }
+
+ private synchronized boolean startRecordingInternal(String phoneNumber, long creationTime) {
+ if (mMediaRecorder != null) {
+ if (DBG) {
+ Log.d(TAG, "Start called with recording in progress, stopping current recording");
+ }
+ stopRecordingInternal();
+ }
+
+ if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.w(TAG, "Record audio permission not granted, can't record call");
+ return false;
+ }
+
+ if (DBG) Log.d(TAG, "Starting recording");
+
+ mMediaRecorder = new MediaRecorder();
+ try {
+ int audioSource = getAudioSource();
+ int formatChoice = getAudioFormatChoice();
+ if (DBG) Log.d(TAG, "Creating media recorder with audio source " + audioSource);
+ mMediaRecorder.setAudioSource(audioSource);
+ mMediaRecorder.setOutputFormat(formatChoice == 0
+ ? MediaRecorder.OutputFormat.AMR_WB : MediaRecorder.OutputFormat.MPEG_4);
+ mMediaRecorder.setAudioEncoder(formatChoice == 0
+ ? MediaRecorder.AudioEncoder.AMR_WB : MediaRecorder.AudioEncoder.AAC);
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Error initializing media recorder", e);
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ return false;
+ }
+
+ String fileName = generateFilename(phoneNumber);
+ Uri uri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ CallRecording.generateMediaInsertValues(fileName, creationTime));
+
+ try {
+ ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
+ if (pfd == null) {
+ throw new IOException("Opening file for URI " + uri + " failed");
+ }
+ mMediaRecorder.setOutputFile(pfd.getFileDescriptor());
+ mMediaRecorder.prepare();
+ mMediaRecorder.start();
+
+ long mediaId = Long.parseLong(uri.getLastPathSegment());
+ mCurrentRecording = new CallRecording(phoneNumber, creationTime,
+ fileName, System.currentTimeMillis(), mediaId);
+ return true;
+ } catch (IOException | IllegalStateException e) {
+ Log.w(TAG, "Could not start recording", e);
+ getContentResolver().delete(uri, null, null);
+ } catch (RuntimeException e) {
+ getContentResolver().delete(uri, null, null);
+ // only catch exceptions thrown by the MediaRecorder JNI code
+ if (e.getMessage().indexOf("start failed") >= 0) {
+ Log.w(TAG, "Could not start recording", e);
+ } else {
+ throw e;
+ }
+ }
+
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+
+ return false;
+ }
+
+ private synchronized CallRecording stopRecordingInternal() {
+ CallRecording recording = mCurrentRecording;
+ if (DBG) Log.d(TAG, "Stopping current recording");
+ if (mMediaRecorder != null) {
+ try {
+ mMediaRecorder.stop();
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Exception closing media recorder", e);
+ }
+
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCurrentRecording.mediaId);
+ getContentResolver().update(uri, CallRecording.generateCompletedValues(), null, null);
+
+ mMediaRecorder = null;
+ mCurrentRecording = null;
+ }
+ return recording;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (DBG) Log.d(TAG, "Destroying CallRecorderService");
+ }
+
+ private String generateFilename(String number) {
+ String timestamp = DATE_FORMAT.format(new Date());
+
+ if (TextUtils.isEmpty(number)) {
+ number = "unknown";
+ }
+
+ int formatChoice = getAudioFormatChoice();
+ String extension = formatChoice == 0 ? ".amr" : ".m4a";
+ return number + "_" + timestamp + extension;
+ }
+
+ public static boolean isEnabled(Context context) {
+ return context.getResources().getBoolean(R.bool.call_recording_enabled);
+ }
+}
diff --git a/java/com/android/dialer/callrecord/res/values/config.xml b/java/com/android/dialer/callrecord/res/values/config.xml
new file mode 100644
index 000000000..7aabd6cac
--- /dev/null
+++ b/java/com/android/dialer/callrecord/res/values/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2015 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>
+ <bool name="call_recording_enabled">false</bool>
+ <!-- 1 (MIC) for microphone audio source (default)
+ 4 (VOICE_CALL) if supported by device for voice call uplink + downlink audio source -->
+ <integer name="call_recording_audio_source">1</integer>
+</resources>
diff --git a/java/com/android/dialer/callrecord/res/xml/call_record_states.xml b/java/com/android/dialer/callrecord/res/xml/call_record_states.xml
new file mode 100644
index 000000000..9907fe9e2
--- /dev/null
+++ b/java/com/android/dialer/callrecord/res/xml/call_record_states.xml
@@ -0,0 +1,1324 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 The LineageOS 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.
+ */
+-->
+<call-record-allowed-flags>
+ <!-- Disable recording for Andorra:
+ Article 183 of the Andorran Penal Code sets a prison sentence of one to four years as the
+ punishment for attempting or succeeding to infringe on the privacy of another person
+ without his or her consent. This includes intercepting calls or using technical means to
+ listen, transmit, record or reproduce their calls. Article 185 and 188 of the Andorran
+ Penal Code describe the length of imprisonment when sharing recordings with third parties,
+ even if one has not partaken in their creation, unless the party was unaware of their
+ illicit origin. The attempt to do any of the above is also punishable by law.
+
+ Penal Code, as of 2014:
+ https://sherloc.unodc.org/res/cld/document/codi_penal_andorra_as_of_2014_html/Andorra_codi_penal_as_of_2014.pdf
+ -->
+ <country iso="ad" allowed="false" />
+
+ <!-- Enable recording for Albania:
+ Relevant laws and/or legal precedents:
+ Article 121, 122 and 123 of the Albanian Penal Code cover the right to privacy, wiretapping
+ and harming other via dissemination of their secrets. Based on these, it is not a criminal
+ offense to record your own calls, but sharing them with a third party or using them to harm
+ the other party in said calls is a criminal offense.
+
+ Penal Code: (nonencrypted link)
+ http://rai-see.org/wp-content/uploads/2015/08/Criminal-Code-11-06-2015-EN.pdf
+ -->
+ <country iso="al" allowed="true" />
+
+ <!-- Enable recording for Armenia:
+ The Armenian Criminal Code discusses the legality and punishment of call recordings, when
+ recorded by a third party, otherwise known as wiretapping. Since the Criminal Code does not
+ specifically mention call recording done by a person that is a party to the call, with or
+ without consent, then the act of doing so is not a criminal offense, as it carries no
+ punishment whatsoever.
+
+ Criminal Code:
+ https://www.unodc.org/res/cld/document/armenia_criminal_code_html/Armenia_Criminal_Code_of_the_Republic_of_Armenia_2009.pdf
+
+ Two examples of usage of call recordings, without persecution:
+ https://fip.am/803
+ https://fip.am/4500
+ -->
+ <country iso="am" allowed="true" />
+
+ <!-- Enable recording for Argentina:
+ Argentina follows the continental law system. If a law does not exist, which defines
+ something as a crime, it is not a crime. Judges in Argentina make decisions based on their
+ reading of the law, and not on precedents. Call recording is currently not defined as a
+ crime in any law. This is defined in Section 19 of the Constitution of Argentina.
+
+ Constitution: (nonencrypted link)
+ http://www.senadoctes.gov.ar/constitucion-arg/Constitution%20of%20the%20Argentine%20Nation.htm
+
+ Example of usage of call recordings as legal evidence:
+ https://www.scribd.com/document/326647534/H-P-C-F-s-recurso-de-casacion
+ -->
+ <country iso="ar" allowed="true" />
+
+ <!-- Enable recording for American Samoa:
+ Federal Law 18 USC § 2511(2)(d) defines the recording of a call as legal when one party to
+ the call agrees to it, if said call recording is not done with the intention of committing
+ a crime. This territory of the United States conforms with its Federal legislation.
+ For further information, check 'us'.
+
+ U.S. Code: Title 18 - Crimes and Criminal Procedure:
+ https://www.law.cornell.edu/uscode/text/18/2511
+ -->
+ <country iso="as" allowed="true" />
+
+ <!-- Enable recording for Austria
+ According to Article 93 (3) of Austrian Communications Law, known as TKG 2003
+ Kommunikationsgeheimnis, it is illegal to recordor pass on information about a call, unless
+ you are one of the parties in that call. While recording is not illegal, sharing the
+ recording might be a punishable offense, without the consent of both sides.
+
+ Communications Law:
+ https://www.jusline.at/gesetz/tkg/paragraf/93
+ https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=20002849
+ -->
+ <country iso="at" allowed="true" />
+
+ <!-- Disable recording for Australia:
+ Australian Capital Territory:
+ Subsection 4(3)(b) Listening Devices Act 1992 (ACT)
+ https://www8.austlii.edu.au/cgi-bin/viewdoc/au/legis/act/consol_act/lda1992181/s4.html
+ A person must not use a listening device with the intention of recording a private
+ conversation to which the person is a party. This does not apply when said listening
+ device is used by, or on behalf of, a party to a private conversation if a principal
+ party to the conversation consents to the listening device being so used, and the
+ recording of the conversation is considered by that principal party, on reasonable
+ grounds, to be necessary for the protection of that principal party's lawful interests;
+ or the recording is not made for the purpose of communicating or publishing the
+ conversation, or a report of the conversation, to any person who is not a party to the
+ conversation.
+
+ New South Wales:
+ Subsection 7(3)(b) Surveillance Devices Act 2007 (NSW)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nsw/consol_act/sda2007210/s7.html
+ A person must not knowingly use a listening device to record a private conversation to
+ which the person is a party. This does not apply when a principal party to the
+ conversation consents to the listening device being so used and the recording of the
+ conversation is reasonably necessary for the protection of the lawful interests of that
+ principal party, or is not made for the purpose of communicating or publishing the
+ conversation, or a report of the conversation, to persons who are not parties to the
+ conversation.
+
+ Northern Territory:
+ Subsection 11(1)(a) Surveillance Devices Act 2007 (NT)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nt/num_act/sda200719o2007256/s11.html
+ Subsection 43, Emergency use of listening device in public interest
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nt/num_act/sda200719o2007256/s43.html
+ It is an offence to use a listening device to record a private conversation to which the
+ person is not a party and the device is used without the express or implied consent of
+ each party to the conversation. Under Section 43, a person may use a listening device to
+ record a private conversation if at the time of use there are reasonable grounds for
+ believing the circumstances are so serious and the matter is of such urgency that the use
+ of the device is in the public interest.
+
+ Queensland:
+ Subsection 43(2)(a) Invasion of Privacy Act 1971 (Qld)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/qld/consol_act/iopa1971222/s43.html
+ A person is guilty of an offence, if the person uses a listening device to record a
+ private conversation and is liable to a maximum penalty of 40 penalty units or
+ imprisonment for 2 years. This does not apply when the person using the listening device
+ is a party to the private conversation.
+
+ South Australia:
+ Subsection 4(2)(a)(ii) Surveillance Devices Act 2016 (SA)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/sa/consol_act/sda2016210/s4.html
+ A person must not knowingly use a listening device to record a private conversation to
+ which the person is, or is not a party. The maximum penalty is $15 000 or imprisonment
+ for 3 years. This does not apply if the use of a listening device is done by a party to
+ the private conversation if the use of the device is reasonably necessary for the
+ protection of the lawful interests of that person.
+
+ Tasmania:
+ Subsection 5(3)(b) Listening Devices Act 1991 (TAS)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/tas/consol_act/lda1991181/s5.html
+ A person shall not use a listening device to record a private conversation to which the
+ person is, or is not, a party. This does not apply when the listening device is used to
+ obtain evidence or information in connection with an imminent threat of serious violence
+ to persons or of substantial damage to property, or a serious narcotics offence, if the
+ person using the listening device believes on reasonable grounds that it was necessary to
+ use the device immediately to obtain that evidence or information. This does not apply if
+ a principal party to the conversation consents to the listening device being so used and
+ the recording of the conversation is reasonably necessary for the protection of the
+ lawful interests of that principal party or the recording of the conversation is not made
+ for the purpose of communicating or publishing the conversation, or a report of the
+ conversation, to persons who are not parties to the conversation.
+
+ Victoria:
+ Subsection 6(1) Surveillance Devices Act 1999 (NSW)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/vic/consol_act/sda1999210/s6.html
+ A person must not knowingly use a listening device to record a private conversation to
+ which the person is not a party. The penalty is up to 2 years imprisonment and up to 240
+ penalty units, or both.
+
+ Western Australia:
+ Subsection 5(3)(d) Surveillance Devices Act 1998 (WA)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/wa/consol_act/sda1998210/s5.html
+ Subsection 26(1)(2)(3)(b)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/wa/consol_act/sda1998210/s26.html
+ A person shall not use a listening device to record a conversation to which that person
+ is, or is not, a party. The penalty is $5 000 or imprisonment for 12 months, or both.
+ This does not apply to the use of a listening device by a party to a private conversation
+ if that principal party consents to its use, as reasonably necessary for the protection
+ of the lawful interests of that principal party. It is also not applicable in cases where
+ a person who is a party to a private conversation, or is acting on behalf of a party to a
+ private conversation, uses a listening device to record said private conversation,
+ believing that the use of the listening device is in the public interest.
+
+ Summary:
+ Most states and territories allow to make a recording of your personal conversations
+ under specific circumstances. The wording of the laws themselves are open to
+ legal interpretation and can be used against users. Until the laws above are presented
+ in a more clear way or enough evidence is shown to substantiate how the courts interpret
+ them when prosecuting private citizens, Australia shall not have call recording enabled.
+ -->
+ <country iso="au" allowed="false" />
+
+ <!-- Enable recording for Bosnia and Herzegovina:
+ Republika Srpska:
+ Article 155 - Unauthorized eavesdropping and tone recording.
+ Paragraph 1 specifically states 'which is not intended for him', which in context means
+ that a person may record anything which is intended for him or her. Paragraph 2 defines
+ the creation of a record with the intent to abuse and/or misuse it, or the act of sharing
+ it with a third party as a criminal offense.
+ Other Articles that might be relevant:
+ Article 153, Privacy of letters, telegrams and others.
+ Article 157, Unauthorized use of Personal Data.
+
+ Federation of Bosnia and Herzegovina:
+ Article 188, Unauthorized Tapping and Sound Recording, defines the recording of any call
+ 'which is not intended for public or private knowledge', as a criminal offense.
+
+ Brčko District:
+ Article 185 of the Criminal Code specifically states 'which was not intended for him',
+ which in context means that a party may record any call which is indended for said party.
+ Wiretapping is considered a crime and is criminally punishable.
+
+ Further information:
+ The Criminal Codes of the legal entities of Bosnia and Herzegovina, namely Republika
+ Srpska, the Federation of Bosnia and Herzegovina and the Brčko District, can all be found
+ through the link below. Please note that these three entities currently have separate
+ laws, due to administrative and/or judicial autonomy. It should be noted that these
+ different entities have over 15 police forces, each with its own jurisdiction.
+ https://www.legislationline.org/documents/section/criminal-codes/country/40/Bosnia%20and%20Herzegovina/show
+ -->
+ <country iso="ba" allowed="true" />
+
+ <!-- Enable recording for Belgium:
+ As stated in the official response below, Belgian law does not consider the recording of
+ one's personal communications as a punishable offense. Using said recordings in a
+ fraudulent and/or demeaning way does carry the potential for liability and/or prosecution
+ by the state. The recording of one's own calls might be regarded as a form of personal data
+ processing, depending on the specifics of the case. Specific laws and cases are quoted
+ within the official response. The Belgian municipality of Baarle-Hertog, consisting of a
+ number of exclaves, has territory which is within the Dutch province of North Brabant, and
+ as such it may not be within the confines of the Belgian ISO and its inherent laws.
+
+ Official Response by Belgian Minister (QRVA 50 157, pages 20199-20202, 24/02/2003):
+ https://www.lachambre.be/QRVA/pdf/50/50K0157.pdf
+ -->
+ <country iso="be" allowed="true" />
+
+ <!-- Enable recording for Bulgaria:
+ Article 32(2) of the Bulgarian Constitution states that it is an inviolable right for
+ people to not be followed, photographed, recorded (audio and/or video) without being
+ notified and/or despite his or her explicit disagreement to said actions, except where the
+ law allows for said actions. The Code of Criminal Procedure, Part III, Articles 125 and
+ 126, page 34, deal with the use of recordings as evidence. No law explicitly tackles the
+ issue of consent when recording one's personal telephone calls. Based on the available
+ documentation and the attached example of lack of state prosecution, recording one's own
+ calls is not legal, nor is it a criminal offense. Personal recordings are unlikely to be
+ a valid form of evidence in a court of law.
+
+ Constitution:
+ https://www.parliament.bg/bg/const
+
+ Code of Criminal Procedure:
+ https://www.mvr.bg/docs/default-source/normativnauredba/3da73fed-npk-pdf.pdf
+
+ Example of lack of state prosecution:
+ https://goo.gl/HQPUup
+ -->
+ <country iso="bg" allowed="true" />
+
+ <!-- Enable recording for Brazil:
+ Call recording is not a criminal offense when it the recording is made by one of the two
+ parties of said call. Interception by a third party is illegal and punishable by law,
+ unless done according to the requirements set out in Law 9296. There may be some debate, as
+ far as the use of a call recording as legitimate evidence. Further information is available
+ in the attached legal discussions below.
+
+ Law 9296 of the 24th of July 1996: (nonencrypted link)
+ http://www.planalto.gov.br/ccivil_03/leis/l9296.htm
+
+ Constitution of Brazil, Art 5º, X and XII:
+ https://www2.camara.leg.br/legin/fed/consti/1988/constituicao-1988-5-outubro-1988-322142-publicacaooriginal-1-pl.html
+
+ Legal discussions:
+ https://direitosbrasil.com/gravar-conversa-e-crime/
+ https://meusitejuridico.com.br/2018/04/02/stj-e-licita-gravacao-de-conversa-feita-pelo-destinatario-de-solicitacao-de-vantagem-indevida
+ https://moisesandrade.jusbrasil.com.br/artigos/121944095/constitucionalidade-do-uso-da-gravacao-clandestina-como-meio-de-prova
+ -->
+ <country iso="br" allowed="true" />
+
+ <!-- Enable recording for Belarus:
+ Article 28 of the Constitution of Belarus covers the right to privacy. Article 179 of the
+ Criminal Code of Belarus covers situations in which a person's privacy is violated by way
+ of any secret being shared without his or her consent, but no specific term of imprisonment
+ or fine is mentioned. The wording of the article is aimed at the collecting and sharing of
+ 'a personal or family secret of another person'. Creating a call recording for personal use
+ is not covered by this article, as privacy is not inherently guaranteed. The use of call
+ recordings as evidence in a court of law is dubious. The sharing of a call recording could
+ be considered as punishable by law, depending on the circumstances.
+
+ Belarusian Constitution: (nonencrypted link)
+ http://www.pravo.by/pravovaya-informatsiya/normativnye-dokumenty/konstitutsiya-respubliki-belarus
+
+ Belarusian Criminal Code:
+ https://etalonline.by/?type=text&regnum=HK9900275#load_text_none_1_
+ -->
+ <country iso="by" allowed="true" />
+
+ <!-- Enable recording for Canada:
+ Any intended recipient of a communication is entitled to record it, based on Section 184(2)
+ Subsection (1) of the Criminal Code of Canada. There are numerous legal cases that validate
+ the interception of private communications by parties to the conversation as not illegal.
+ For a more in-depth look, refer to the LegalTree article below.
+
+ Criminal Code:
+ https://laws-lois.justice.gc.ca/eng/acts/C-46/page-1.html
+
+ LegalTree article:
+ https://legaltree.ca/node/908
+
+ Legal articles:
+ https://lambertavocatinc.com/avocat-montreal/enregistrer-conversation-legal/
+ https://www.avocat.qc.ca/affaires/iitelephone.htm
+ -->
+ <country iso="ca" allowed="true" />
+
+ <!-- Disable recording for Switzerland:
+ According to Article 179 of the Swiss Criminal Code of 21 December 1937, it is a criminal
+ offense to store, record, or share the recording of a call, even when one is part of said
+ call. Explicit consent is required by both parties for a recording to be legal.
+
+ Criminal Code:
+ https://www.admin.ch/opc/en/classified-compilation/19370083/index.html#a179ter
+ -->
+ <country iso="ch" allowed="false" />
+
+ <!-- Enable recording for Chile:
+ The Chilean law is considered a type of civil law, hence judges base their decisions on
+ their own reading of the law. The Chilean Supreme Court ruled in favor of accepting
+ one-party consent call recording as a form of legal evidence, hence the act of recording
+ your own calls is not criminally punishable, as can be seen in the Chilean Penal Code.
+
+ Penal Code:
+ https://www.leychile.cl/Navegar?idNorma=1984
+
+ Article on one-party consent:
+ https://radio.uchile.cl/2018/04/22/grabacion-es-aceptada-como-prueba-en-juicio-por-practicas-antisindicales/
+ -->
+ <country iso="cl" allowed="true" />
+
+ <!-- Enable recording for China:
+ No clear definition exists on the matter of call recordings being made by a private
+ citizen within the Criminal Law of the People's Republic of China. Depending on whether
+ said call recording was made and/or published with malicious intent, it may or may not be
+ admissible in court. For further information
+
+ Criminal Law of the People's Republic of China: (nonencrypted link)
+ http://english.court.gov.cn/2015-12/01/content_22595464.htm
+
+ Supreme People's Court Provisions on Evidence in Civil Procedures: (nonencrypted link)
+ http://en.pkulaw.cn/display.aspx?cgid=38083&lib=law
+ -->
+ <country iso="cn" allowed="true" />
+
+ <!-- Enable recording for Costa Rica:
+ One may record one's own calls, as long as they are calls between said person and only one
+ other party, that is two say two sides. Calls between 3 or more people can not be legally
+ recorded without all sides agreeing to one person doing so, as long as said person is a
+ part of the call and not wiretapping or eavesdropping. Recording calls with more than 2
+ participants requires the express consent of all other parties. Article 29 of the
+ Communication Law of 1994 specifies under what circumstances one may or may not do so.
+
+ Communication Law: (nonencrypted link)
+ http://www.pgrweb.go.cr/scij/Busqueda/Normativa/Normas/nrm_texto_completo.aspx?param1=NRM&nValor1=1&nValor2=16466&strTipM=FN
+
+ Article:
+ https://www.laprensalibre.cr/Noticias/detalle/75929/ojo-conversaciones-grabadas-pueden-usarse-como-prueba-en-juicio
+ -->
+ <country iso="cr" allowed="true" />
+
+ <!-- Enable recording for Cyprus:
+ The Cypriot Penal Code does not explicitly cover the act of wiretapping or recording one's
+ own calls. Based on this, it is not a criminal offense to record personal calls. Article
+ 369 of the Cypriot Penal Code states that anyone who knows that another is planning to
+ commit a criminal offense, yet fails to use any reasonable means to prevent said crime, is
+ guilty of misconduct, which can be used as a reason for recording one's own calls, should
+ the need arise to quote a legal document.
+
+ Penal Code: (nonencrypted link)
+ http://www.cylaw.org/nomoi/enop/non-ind/0_154/index.html
+ -->
+ <country iso="cy" allowed="true" />
+
+ <!-- Enable recording for Czech Republic:
+ Case Law File Number 21 502/2000 of the Supreme Court specifies that even when evidence is
+ acquired or provided in contravention to legal regulations and/or personal rights, it shall
+ not be deemed as inadmissible. This, as well as other information, is accessible in the
+ Constitutional Court Finding 191/05 of the 13th of September 2006.
+
+ Constitutional Court Finding:
+ https://nalus.usoud.cz/Search/GetText.aspx?sz=1-191-05_2
+ -->
+ <country iso="cz" allowed="true" />
+
+ <!-- Disable recording for Germany:
+ According to Section 201 of the German Criminal Code - Violation of the privacy of the
+ spoken word, making an audio recording of the privately spoken words of another or making
+ such a recording accessible by a third party will result in up to three years of
+ imprisonment. Article 10 of the German Constitution explicitly states that the secrecy of
+ telecommunications is inviolable. There are notable exceptions, such as the use of
+ recordings when in a legitimate self-defense situation. Article 227 of the German Civil
+ Code notes that acting in one's own defense is not unlawful, which is also explained in
+ Article 32 of the German Criminal Code. Article 88 of the Telecommunications Act defines
+ telecommunications secrecy. The German municipality of Büsingen am Hochrhein is an exclave
+ within the territorial confines of Switzerland, and as such it may not be within the
+ confines of the German ISO and its inherent laws.
+
+ Civil Code:
+ https://www.gesetze-im-internet.de/bgb/__227.html
+
+ Criminal Code:
+ https://www.gesetze-im-internet.de/stgb/__32.html
+
+ Constitution:
+ https://www.gesetze-im-internet.de/gg/art_10.html
+
+ Telecommunications Act:
+ https://www.gesetze-im-internet.de/tkg_2004/__88.html
+
+ Wikipedia article on self-defense laws in Germany:
+ https://de.wikipedia.org/wiki/Notwehr_(Deutschland)
+
+ Explanation of lawful use of a recording in a legal dispute:
+ https://www.anwalt.de/rechtstipps/gespraechsmitschnitte-als-beweismittel-ungeeignet_057458.html
+ -->
+ <country iso="de" allowed="false" />
+
+ <!-- Enable recording for Denmark, Faroe Islands and Greeenland:
+ Chapter 27, Article 263(3) of the Criminal Code of Denmark denotes that a person is liable
+ for criminal punishment when he or she intercepts or records telephone conversations to
+ which he or she is not a party. The articles in Chapter 27 cover a lot of different
+ situations, including the dissemination of recordings, which may lead to a fine or prison
+ sentence. The act of recording a conversation that one is a part of is not covered
+ explicitly, hence it is not a criminal offense in the eyes of the law.
+
+ Criminal Code:
+ https://www.retsinformation.dk/Forms/r0710.aspx?id=164192#Kap27
+ -->
+ <country iso="dk,fo,gl" allowed="true" />
+
+ <!-- Enable recording for Estonia:
+ Recording your calls for personal use is not a criminal offense. Sharing said calls with a
+ third party is a criminal offence, hence punishable by law, except in cases where said
+ calls are shared by a journalist.
+
+ Constitution, Paragraph 43:
+ https://www.pohiseadus.ee/index.php?sid=1&p=43
+
+ Instructions for call recording (GDPR equivalent):
+ https://www.aki.ee/sites/www.aki.ee/files/elfinder/article_files/Telefonik%C3%B5nede%20salvestamise%20lubatavuse%20juhend.pdf
+
+ Legal article:
+ https://digi.geenius.ee/rubriik/uudis/millistel-juhtudel-tohib-eestis-telefonikone-salvestada-ja-selle-sisu-avaldada/
+ -->
+ <country iso="ee" allowed="true" />
+
+ <!-- Enable recording for Spain:
+ Based on the decision of the Spanish Constitutional Tribunal of November the 29th, 1984, it
+ is legal for a party to record his or her calls without notifying the other party. Sharing
+ said recording with a third party is not protected and may make the party that has shared
+ the recording liable to a civil suit, to be initiated by the aggrieved party. Unless done
+ so for judicial purposes, it is punishable to disclose or share the recording or the gist
+ of the recording to other parties. The town of Llívia is a Spanish exclave within the
+ territory of the Republic of France, and as such, it may not be within the confines of the
+ Spanish ISO and its inherent laws.
+
+ Decision of the Spanish Constitutional Tribunal:
+ https://hj.tribunalconstitucional.es/eu/Resolucion/Show/367
+
+ Legal articles: (nonencrypted link)
+ http://belegal.com/blog-by-antonio-flores/validity-of-recorded-telephone-conversations-in-spain/
+ https://www.fonvirtual.com/blog/la-grabacion-de-llamadas/
+ https://www.legalisconsultores.es/2014/04/es-legal-realizar-grabaciones-su-aportacion-en-juicios/
+ -->
+ <country iso="es" allowed="true" />
+
+ <!-- Enable recording for Finland:
+ As a private citizen, one may record any call they participate in. There is no requirement
+ to make other parties aware of the recording, but the use of said recordings, depending on
+ their content, may be subject to various laws, such as data protection (privacy)
+ legislation, libel laws, laws governing trade and national secrets, non-disclosure
+ agreements and so on.
+
+ Bureau of Data Ombudsman:
+ https://web.archive.org/web/20180517050133/http://www.tietosuoja.fi/sv/index/useinkysyttya/puheluidennauhoittaminen.html
+ -->
+ <country iso="fi" allowed="true" />
+
+ <!-- Enable recording for France, Saint Barthélemy, French Guayana, Guadeloupe, Saint Martin,
+ Martinique, New Caledonia, French Polynesia, Saint Pierre & Miquelon,
+ Réunion, Wallis-et-Futuna and Mayotte:
+ While recording calls without consent, as a third party, is punishable, it depends on
+ whether said recording was created or used with a malicious intent. Judges are free to view
+ said recordings as a form of evidence and base their final decisions with their help.
+ Recording your own calls as a private citizen is not a criminal offense. Sharing said
+ recordings, with the intent to harm the other party in any way, is a criminal offense.
+
+ Penal Code:
+ https://www.legifrance.gouv.fr/affichCode.do;jsessionid=3E84EAC0F63D49FC16A28B8D90EFF1D2.tplgfr44s_2?idSectionTA=LEGISCTA000006165309&cidTexte=LEGITEXT000006070719&dateTexte=20150413
+
+ Civil Code:
+ https://www.legifrance.gouv.fr/affichCode.do;jsessionid=1A7384A63066DBE1E1D8C732E698F844.tplgfr23s_3?idSectionTA=LEGISCTA000006117610&cidTexte=LEGITEXT000006070721&dateTexte=20190606
+
+ Legal article on call recordings as evidence:
+ https://www.annuaireavocats.fr/articles/enregistrer-une-conversation-a-linsu-dune-personne-est-ce-legal
+
+ Legal article on recording in the workplace:
+ https://www.cnil.fr/fr/lecoute-et-lenregistrement-des-appels-sur-le-lieu-de-travail
+ -->
+ <country iso="fr,bl,gf,gp,mf,mq,nc,pf,pm,re,wf,yt" allowed="true" />
+
+ <!-- Enable recording for United Kingdom:
+ Recording one's own calls is not a criminal offence and is not prohibited. As long as the
+ recording is for personal use, consent and/or notification of the other party are not
+ required. Call recordings can be used as evidence, since it is based on a trite law.
+ Sharing said call recordings with a third party, without consent, may be a criminal offence
+ and punishable.
+
+ Use as evidence (p. 3):
+ https://www.bailii.org/uk/cases/UKPC/1954/1954_43.pdf
+
+ Legal articles:
+ https://www.computertel.co.uk/article?ref=Call-Recording-Law-in-the-UK-2018-edition
+ https://www.dma-law.co.uk/is-it-illegal-to-record-conversations/
+ -->
+ <country iso="gb" allowed="true" />
+
+ <!-- Enable recording for Georgia:
+ The Constitution of Georgia, Chapter Two - Fundamental Human Rights, Article 15(2) states
+ that personal communication(s) are inviolable and that said right may only be restricted in
+ accordance with the law, to ensure national security or public safety, or to protect the
+ rights of other parties, insofar as it is necessary in a democratic society, based on a
+ court decision or without a court decision in cases of urgent necessity, as provided by the
+ law. Articles 157, 158 and 159 of the Criminal Code of Georgia deal with the disclosure of
+ private information, personal data, the violation of the secrecy of private communication
+ and the violation of secrecy of personal correspondence, phone conversations or other kinds
+ of communication. The document does not specify a situation in which one side of a
+ conversation records without the other side's knowledge or consent, thus the act of
+ recording one's conversations is in a legally gray area. All of the above articles
+ explicitly note that no criminal liability can be incurred if the gathered information is
+ submitted to investigative authorities.
+
+ Constitution:
+ https://matsne.gov.ge/en/document/view/30346?publication=35
+
+ Criminal Code of Georgia:
+ https://matsne.gov.ge/en/document/view/16426?publication=187&scroll=62067
+ -->
+ <country iso="ge" allowed="true" />
+
+ <!-- Enable recording for Greece:
+ Section 2 of Article 370A of the Greek Penal Code bans it, subarticle 4 offers exceptions
+ when no other evidence is present. Decision 53/2010 of the Supreme Criminal Court limits
+ evidence submitting to third parties that found the recording 'by accident'. Decision
+ 277/2014 of the Supreme Criminal Court acquitted a guilty party and deemed the presented
+ recordings admissable. Article 25 of the Penal Code states that, any action is not illegal
+ if it was done so to protect the property or safety of oneself or of another party,
+ provided that the crime of sharing the recording is a lesser one in comparison.
+
+ Legal discussion:
+ https://uk.practicallaw.thomsonreuters.com/w-010-1738
+ -->
+ <country iso="gr" allowed="true" />
+
+ <!-- Enable recording for Croatia:
+ Article 143 of the Croatian Criminal Code, Paragraph 1 notes that the recording of another
+ person's privately uttered words is a criminal offense, when said words are not 'intended
+ for his or her attention' and could lead to imprisonment not exceeding three years.
+ Paragraph 2, which holds the same punishment, indicated that situations in which the
+ recording, its transcription or the 'gist' of said recording being shared as an equal
+ crime. Paragraph 4 states that there is no criminal offence if said acts are committed in
+ 'the public interest or another interest prevailing over the interest to protect the
+ privacy of the person being recorded or eavesdropped on'. Prosecution is made per request
+ and the state does not initiate it, which renders the matter to the level of a civil case
+ and not to that of a criminal case.
+
+ Croatian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/37/Croatia/show
+ -->
+ <country iso="hr" allowed="true" />
+
+ <!-- Enable recording for Hungary:
+ Sections 413 and 418 define the Breach of Trade and/or Business Secrecy as a criminal
+ offense. There is no mention of wiretapping and/or eavesdropping as a criminal offense.
+ The Hungarian Data Protection and Freedom of Information Agency (DPA) created a Guidance in
+ 2016, for cases concerning situations which include an individual as one side of the
+ conversation, and a data processing entity as the other side. This guidance should not be
+ considered relevant, as it does not deal with the communications of individuals. No
+ pertinent articles or paragraphs were found in the Hungarian Criminal Code, which in effect
+ equates to there being no punishment for the recording of personal calls.
+
+ DPA 2016 Guidance:
+ https://www.naih.hu/files/2016_05_09_tajekoztato_hangfelvetelekrol.pdf
+
+ Hungarian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/25/Hungary/show
+ -->
+ <country iso="hu" allowed="true" />
+
+ <!-- Disable recording for Indonesia:
+ Based on Article 26 of both Law Number 11 of 2008 and its revision, Law Number 19 of 2016,
+ call recording is defined on its own and requires one to obtain consent from the other
+ party when recording calls, although it can be used as a form of evidence. Whether
+ recording another person without his or her consent is a criminal offense that is
+ prosecuted by the country itself is not clear and further information should be gathered by
+ a native speaker.
+
+ Law 11 of 2008 (first file):
+ https://www.hukumonline.com/pusatdata/detail/27912/nprt/1011/uu-no-11-tahun-2008-informasi-dan-transaksi-elektronik
+
+ Law 19 of 2016:
+ https://jdih.kominfo.go.id/produk_hukum/view/id/555/t/undangundang+nomor+19+tahun+2016+tanggal+25+november+2016
+ -->
+ <country iso="id" allowed="false" />
+
+ <!-- Enable recording for Ireland:
+ The Irish Constitution does not specifically state a right to privacy. Subsection (6) of
+ section 98 of the Interception of Postal Packets and Telecommunications Messages
+ (Regulation) Act of 1993 defines interception of a call in such a way, that deems the
+ recording of a call by one party to the call legal. Whether said call recording can be used
+ as evidence or infringes upon a person's privacy is a complicated matter that can only be
+ decided on a case-by-case basis. Subsection (2) of section 98 goes on to elabore on cases
+ in which call recordings are legal, such as in the interests of the security of the State
+ (c), for the prevention or detection of crime or for the purpose of any criminal
+ proceedings (b) and others.
+
+ Telecommunications Messages Act of 1993: (nonencrypted link)
+ http://www.irishstatutebook.ie/eli/1993/act/10/enacted/en/print.html
+
+ Legal discussions:
+ https://www.mhc.ie/latest/insights/big-brother-is-watching-but-is-he-listening-too
+ https://www.irishtimes.com/news/crime-and-law/q-a-what-are-the-legal-implications-1.1740070
+ -->
+ <country iso="ie" allowed="true" />
+
+ <!-- Enable recording for Israel and Palestine:
+ Israeli law specifies that call recording is illegal and punishable when neither party in
+ said conversation is aware of said act of recording. Either party in a conversation can
+ record his or her calls without being legally required to inform the other party.
+ Due to legal ambiguity, it is currently impossible to determine which set of laws should be
+ taken under consideration when recording personal calls within the Palestinian territories.
+ This is relevant as the Occupied Palestinian Territory makes use of the Mobile Country Code
+ registered to Israel. Palestine's ISO is set as disabled, since if it is in use there is no
+ legal way to determine which set of laws are being used, due to the differing laws used
+ in parts of it.
+
+ The Wiretapping Law, 5739-1979:
+ https://www.nevo.co.il/law_html/law01/077_001.htm
+
+ Information on State of Palestine:
+ https://en.wikipedia.org/wiki/Palestinian_law#Statutes_and_legislation
+
+ News articles:
+ https://www.globes.co.il/news/article.aspx?did=1001066185
+ https://www.ynet.co.il/articles/0,7340,L-3043583,00.html
+ -->
+ <country iso="il" allowed="true" />
+ <country iso="ps" allowed="false" />
+
+ <!--Enable recording for India:
+ No clear definition exists on the matter of call recordings being made by one side.
+ Depending on whether said call recording was made and/or published with malicious intent,
+ it may or may not be admissible in court, and/or punishable by law. There are a number of
+ precedents and legal definitions, which are available below.
+
+ Legal discussion:
+ https://copyright.lawmatters.in/2012/02/recording-telephonic-conversations.html
+ -->
+ <country iso="in" allowed="true" />
+
+ <!-- Disable recording for Iceland:
+ According to the Electronic Communications Act, No. 81, recording one's own telephone
+ conversations without notifying the other party can make the recording party liable to
+ fines or imprisonment of up to six months in the case of serious or repeated violations, as
+ explicitly stated in Article 74. Article 48 covers the Recording of telephone calls and
+ states that the party to a telephone conversation that wishes to record said conversation
+ shall, when it commences, notify the opposite party of his or her intent to do so. This is
+ not required when the opposite party can clearly be assumed to be aware of the recording.
+
+ Electronic Communications Act:
+ https://www.government.is/Publications/Legislation/Lex/?newsid=86c9a6a9-fab5-11e7-9423-005056bc4d74
+ -->
+ <country iso="is" allowed="false" />
+
+ <!-- Enable recording for Iran:
+ Based on Article 25 of the Iranian Constitution, recording one's own calls, as a private
+ citizen for archival reasons, is not illegal. The sharing of said recordings with a third
+ party is forbidden based on the aforementioned legal document. According to Article 730 of
+ the Iranian Cybercrime Law, wiretapping a call which can be defined as non-public is a
+ crime and may lead to a punishment in the form of imprisonment for a period of six months
+ to two years, or a fine of ten to forty million rials, or both. There is currently no
+ punishment for the act of recording one's own calls in the Iranian Penal Code, thus the act
+ itself is not criminally punishable. Sharing said recordings in a way that causes injury to
+ the other party might be criminally punishable. Caution is advised, due to the geopolitical
+ situation surrounding the Islamic Republic of Iran.
+
+ Constitution:
+ https://www.wipo.int/edocs/lexdocs/laws/en/ir/ir001en.pdf
+ Penal Code of 2013 (in Persian):
+ https://www.refworld.org/cgi-bin/texis/vtx/rwmain/opendocpdf.pdf?reldoc=y&docid=5447c9274
+ Cybercrime Law (in Persian):
+ https://www.cyberpolice.ir/page/42981
+ Legal article (in Persian):
+ https://www.irna.ir/news/83268974/%D8%B6%D8%A8%D8%B7-%D9%85%D9%83%D8%A7%D9%84%D9%85%D9%87-%D8%AA%D9%84%D9%81%D9%86%DB%8C-%D8%AC%D8%B1%D9%85-%D9%86%DB%8C%D8%B3%D8%AA
+ -->
+ <country iso="ir" allowed="true" />
+
+ <!-- Enable recording for Italy and Vatican City State:
+ It is not illegal to record a conversation, as parties to calls automatically accept the
+ risk that a call may be recorded. Making a recording available to other parties is a
+ criminal offense, when done so for reasons other than protecting either one's own rights or
+ other parties' rights. Articles 23 and 167, in the Privacy Code, deem that the crimes
+ provided for therein are punishable only if said acts result in harm. According to the
+ Supreme Court of Cassation, recorded conversations are legal and can be used as evidence in
+ court, even if the other party is unaware of being recorded, provided that it is not
+ recorded by a third party. The Italian comune of Campione d'Italia features an exclave,
+ situated within the Swiss canton of Ticino, and as such it may not be within the confines
+ of the Italian ISO and its inherent laws.
+
+ Legal articles:
+ https://www.altalex.com/index.php?idnot=53369
+ https://web.archive.org/web/20161011100301/http://notizie.tiscali.it/socialnews/articoli/polimeni/13230/registrare-di-nascosto-per-la-cassazione-e-legale/
+ -->
+ <country iso="it,va" allowed="true" />
+
+ <!-- Enable recording for Japan:
+ Recording one's own calls is neither a criminal offense, nor illegal. Wiretapping and
+ leaking information gained from a recording is illegal and may be criminally punishable.
+ Recording as a third party is a criminal offense, when done so without the consent of at
+ least one party to the conversation. Recordings obtained without consent from both sides
+ will not be admitted as evidence in a criminal case, but are admitted as such in most civil
+ cases, unless it was obtained in a method, which the court deems as unacceptable. If the
+ recording infringes one's personal rights or discloses trade secrets, sharing said
+ recording might lead to civil cases. In work-related instances, one may record and divulge
+ information under the protection of the Whistleblower Protection Act of 2004. The Supreme
+ Court of Japan's Decision of the 12th of July 2000, case number 1999 (A) 96, was in favor
+ of admitting a tape recording as evidence, which was made by one party to a conversation,
+ without the other party's consent.
+
+ Whistleblower Protection Act: (nonencrypted link)
+ http://drasuszodis.lt/userfiles/Japan%20Whistleblower%20Protection%20Act.pdf
+
+ Decision of the Supreme Court of Japan: (nonencrypted link)
+ http://www.courts.go.jp/app/hanrei_en/detail?id=494
+
+ Legal articles: (nonencrypted link)
+ https://www.moneypost.jp/292939
+ https://president.jp/articles/-/15666
+ https://www.hrpro.co.jp/trend_news.php?news_no=636
+ https://kumaben.com/recording-audio-without-consent/
+ https://www.mot-net.com/blog/efficiency-of-operations/6737
+ https://milight-partners-law.hatenablog.com/entry/2015/08/31/152333
+
+ Legal discussion:
+ https://blogs.yahoo.co.jp/unyieldingspirit2007/24529523.html
+ -->
+ <country iso="jp" allowed="true" />
+
+ <!-- Enable recording for South Korea:
+ According to Article 3(1) of the Protection of Communications Secrets Act, it is forbidden
+ to wiretap, record or listen to any conversation between other parties. Article 4 defines
+ recordings obtained by way of illegal recording or wiretapping as inadmissible, hence they
+ can not be used as evidence in a trial or disciplinary procedure. Article 14 goes on to
+ specify that no person shall record a conversation between other parties, that is not
+ public, or listen to said parties' conversation through the use of electronic or mechanical
+ devices. Definitions of recording, wiretapping and other such terms may be found in Article
+ 2. The Protection of Communications Secrets Act clearly defines that recording is not legal
+ when done by a third party, but does not specifically discuss whether whether both parties
+ to a conversation need to agree to a recording. Since there is no penalty listed, recording
+ one's own conversations should be in, at worst, a gray area that should still not make the
+ act punishable. Similarly, whether recordings made without consent can be used as evidence
+ is legally unclear.
+
+ Protection of Communications Secrets Act:
+ https://elaw.klri.re.kr/kor_service/lawView.do?hseq=31731&lang=ENG
+ -->
+ <country iso="kr" allowed="true" />
+
+ <!-- Enable recording for Liechtenstein:
+ Recording a call between an organization and an individual is illegal, when done without
+ notification and/or consent. Recording a call between individuals is illegal and punishable
+ when transmitting said recording or information to a third party, and/or when the person
+ that initiates the recording is not part of the conversation. This means that recording a
+ call when you are one of the two parties is legal, even without notifying the other party.
+ Legal action must be initiated by the aggrieved party. The following is defined in Article
+ 120 of the Criminal Code of 24 June 1987 (StGB), points 1, 2, 2a and 3. Article 100 of the
+ Constitution may be pertinent to use of call recordings as evidence.
+
+ Criminal Code of 24 June 1987 (StGB):
+ https://www.regierung.li/media/medienarchiv/311_0_11_07_2017_en.pdf.
+
+ Constitution:
+ https://www.regierung.li/media/medienarchiv/101_01_01_2012_en.pdf?t=2.
+ -->
+ <country iso="li" allowed="true" />
+
+ <!-- Enable recording for Sri Lanka:
+ Part IV/59 of the Sri Lankan Telecommunications Act defines the penalty for eavesdropping
+ on a call. The Sri Lankan Penal Code does not cover the act of recording one's own calls,
+ hence the act is not criminally punishable.
+
+ Telecommunications Act:
+ https://www.lawnet.gov.lk/1947/12/31/sri-lanka-telecommunications-2/
+
+ Penal Code:
+ https://www.lawnet.gov.lk/penal-code-consolidated-2/
+
+ Article: (nonencrypted link)
+ http://www.dailymirror.lk/article/PTL-tampered-with-phone-recording-system-ASG-135574.html
+ -->
+ <country iso="lk" allowed="true" />
+
+ <!-- Enable recording for Lithuania:
+ Article 166 of the Lithuanian Criminal Code defines that violations of a person's
+ correspondence, by unlawfully wiretapping a person's conversations as a criminal offense,
+ which could lead to a term of imprisonment of up to two years, a fine or community service.
+ The wording of said article is unclear and only mentions electronic communication networks
+ and recording and/or wiretapping as a third party, and not as one of the two parties.
+ Article 61 of the Law on Electronic Communications defines confidentiality of
+ communications, as far as situations like those covered by GDPR, as in the handling of
+ information between individuals and legal entities, and should therefore not be taken into
+ account.
+
+ Lithuanian Criminal Code:
+ https://e-seimas.lrs.lt/portal/legalActPrint/lt?jfwid=q8i88l10w&documentId=a84fa232877611e5bca4ce385a9b7048&category=TAD
+
+ Lithuanian Law on Electronic Communications:
+ https://e-seimas.lrs.lt/portal/legalActPrint/lt?jfwid=-wd7z7kkgy&documentId=05cd4e020f0a11e7b6c9f69dc4ecf19f&category=TAD
+ -->
+ <country iso="lt" allowed="true" />
+
+ <!-- Enable recording for Luxembourg:
+ The Luxembourgish Penal Code does not specifically cover the right to privacy and its
+ infringement. Based on this, it is not a criminal offense to record one's personal calls,
+ although doing so in a public manner may lead to a civil case from the aggrieved party. One
+ should consult further with a lawyer whether sharing said recording or recordings would
+ constitute a criminal offense.
+
+ Penal Code: (nonencrypted link)
+ http://legilux.public.lu/eli/etat/leg/code/penal/20181101
+ -->
+ <country iso="lu" allowed="true" />
+
+ <!-- Enable recording for Latvia
+ There is no clear definition of call recording by itself within the Criminal Law of Latvia.
+ Article 144 of said law covers breach of information secrecy, when said information is in
+ the form of correspondence or data relayed by way of electronic communications networks.
+ Paragraph (1) defines the punishment for violating the secret of a person's correspondence
+ as a term of imprisonment for up to two years, or a fine, or others. In a 2014
+ e-Consultation, the Deputy Head of the Public Relations Department of the State Police,
+ Tom Sadovsky, defined the recording of calls with the intent to use as evidence as legal.
+ The Personal Data Protection Law does not apply, as it considers the communication between
+ individuals and legal entitites.
+
+ Latvian Criminal Law:
+ https://likumi.lv/doc.php?id=88966
+
+ Latvian Personal Data Protection Law:
+ https://likumi.lv/doc.php?id=4042
+
+ Legal consultation:
+ https://lvportals.lv/e-konsultacijas/4460-sarunas-drikst-ierakstit-2014.
+ -->
+ <country iso="lv" allowed="true" />
+
+ <!-- Enable recording for Morocco:
+ Call recording is not punishable as one side of a two-party conversation. Recordings are
+ not admissible in court, if the other party is not aware of the recording. Article 447 of
+ the Criminal Law of Morocco, states that the premeditated and unconsented publication of
+ video and/or audio files is a punishable offense.
+
+ Personal Data Law 09-08:
+ https://www.afapdp.org/wp-content/uploads/2018/05/Maroc-Loi-09-08-relative-a-la-protection-des-personnes-physiques-a-legard-du-traitement-des-DCP-2009.pdf
+
+ Moroccan Criminal Law:
+ https://www.h24info.ma/maroc/la-loi-sur-la-protection-des-donnees-personnelles-entre-en-vigueur-le-13-septembre/
+ -->
+ <country iso="ma" allowed="true" />
+
+ <!-- Disable recording for Monaco:
+ According to the Penal Code of Monaco, Article 308-2, a person may be punished with a
+ prison sentence of six months to three years, as well as a fine, for infinging or
+ attempting to infringe on a person's rights to privacy. This includes wiretapping,
+ recording or transmitting the words spoken by a person in a private place. Consent will be
+ presumed when such an action is done during a meeting, with the knowledge of the person
+ that is being recorded. Article 344 of the Penal Code mentions the same punishment for
+ purposeful wiretapping.
+
+ Penal Code:
+ https://www.legimonaco.mc/305/legismclois.nsf/ViewCode!OpenView&Start=1&Count=300&RestrictToCategory=CODE%20P%C3%89NAL
+ -->
+ <country iso="mc" allowed="false" />
+
+ <!-- Enable recording for Moldova:
+ Article 30 of the Constitution of Moldova ensures the privacy of correspondence. No
+ specific law has been enacted that defines recording calls, as an individual, as a criminal
+ offense. There are laws which define this for legal entities and for the government. Please
+ read the attached legal discussion for further information on the subject.
+
+ Constitution of Moldova:
+ https://www.presedinte.md/eng/constitution
+
+ Code of Criminal Procedure:
+ https://www.seepag.info/download/rep_moldova/Criminal%20Procedure%20Code%20RM.pdf
+
+ Regulations for legal entities:
+ https://www.anrceti.md/files/filefield/hca%20nr.48%20din%2010.09.2013%20regulam%20priv%20serv%20CE.pdf
+
+ Legal discussion:
+ https://jsa.md/2017/02/06/inregistrarea-convorbirilor-telefonice-cit-de-legala-este/
+ -->
+ <country iso="md" allowed="true" />
+
+ <!-- Enable recording for Montenegro:
+ Article 173 of the Criminal Code of Montenegro marks call recording as legal if the content
+ of the conversation was 'intended for your use'. It is also legal when it concerns the
+ prevention of crimes, which carry a sentence of 5 years minimum. Sharing a conversation to
+ a third party is a criminal offense.
+
+ Criminal Code of Montenegro:
+ https://www.pravda.gov.me/ResourceManager/FileDownload.aspx?rid=256001&rType=2&file=Krivi%C4%8Dni%20zakonik%20Crne%20Gore.pdf
+ -->
+ <country iso="me" allowed="true" />
+
+ <!-- Enable recording for North Macedonia:
+ As stated in the Macedonian Penal Code, if the recording is made available to a third party
+ or is created and/or distributed with a malicious intent, then the other party can sue you.
+ The state does not prosecute in such cases, unless the act is done by an official state
+ representative of any kind, as mentioned in 151.4 and 151.5.
+
+ North Macedonian Penal Code:
+ https://www.wipo.int/edocs/lexdocs/laws/mk/mk/mk018mk.pdf
+ -->
+ <country iso="mk" allowed="true" />
+
+ <!-- Enable recording for Malta:
+ Relevant laws and/or legal precedents:
+ Article 34 (1)(f) of the Maltese Constitution states that a person may be deprived of his
+ rights in the case of there being suspicion of said person having commited, or being in the
+ process of committing a crime. Effectively this means that recording your own calls is
+ legal when done so to report a crime. There is no mention of the act of recording one's own
+ calls in the Maltese Criminal Code, which means that even if it were to be illegal, it is
+ not a criminal offense. The Media and Defamation Act of 2018 handles all cases of
+ defamation, which may or may not include the act of publishing one's call recordings
+ without the knowledge or consent of the other concerned party.
+
+ Constitution: (nonencrypted link)
+ http://www.justiceservices.gov.mt/DownloadDocument.aspx?app=lom&itemid=8566&l=1
+
+ Criminal Code: (nonencrypted link)
+ https://www.justiceservices.gov.mt/DownloadDocument.aspx?app=lom&itemid=8574&l=1
+
+ Media and Defamation Act: (nonencrypted link)
+ http://justiceservices.gov.mt/DownloadDocument.aspx?app=lp&itemid=29045&l=1
+ -->
+ <country iso="mt" allowed="true" />
+
+ <!-- Enable recording for Netherlands, Bonaire, Sint Eustatius, Saba, Sint Maarten, Curaçao,
+ Aruba:
+ Recording one's own conversations without the consent of the other party or parties is not
+ in itself punishable by law. Sharing recordings made without consent is punishable in the
+ form of a libel case. This in effect means that the government shall not prosecute anyone
+ for the recording of calls. Call recordings may be used as evidence in criminal and civil
+ cases.
+
+ Legal discussion:
+ https://blog.wetrecht.nl/telefoongesprekken-opnemen-als-bewijs-kan-dat
+ -->
+ <country iso="nl,bq,sx,cw,aw" allowed="true" />
+
+ <!-- Enable recording for Norway:
+ As a private citizen, one may record any call that they participate in. There is no
+ requirement to make other parties aware of the recording, but the use of said recording(s),
+ depending on the content, may be subject to various laws, such as data protection (privacy)
+ legislation, libel laws, laws governing trade and national secrets, non-disclosure
+ agreements and so on. It is, however, prohibited to record calls without the permission of
+ the other party or parties, if you are making the call on behalf of a company or
+ organization. All of the above is outlined in Article 205 of the Norwegian Penal Code.
+
+ Penal Code:
+ https://lovdata.no/dokument/NL/lov/2005-05-20-28/KAPITTEL_2-6#§205
+
+ Legal article:
+ https://www.datatilsynet.no/regelverk-og-verktoy/veiledere/lydopptak/
+ -->
+ <country iso="no" allowed="true" />
+
+ <!-- Enable recording for New Zealand:
+ According to the Crimes Act of 1961, Public Act 216B, Articles 1 and 2(a), anyone is liable
+ to imprisonment for a term not exceeding 2 years for intentionally intercepting any private
+ communication, unless he or she is a party to that private communication. Public Act 216C,
+ subsections (1) and (2) define the prohibition on disclosure of unlawfully intercepted
+ private communications. The recording of one's personal conversations and their publishing
+ or use as evidence without the other party's consent is not explicitly forbidden, nor is it
+ defined as a criminal offense.
+
+ Crimes Act of 1961, Part 9A, Crimes against personal privacy: (nonencrypted link)
+ http://www.legislation.govt.nz/act/public/1961/0043/latest/DLM327382.html#DLM329802
+ -->
+ <country iso="nz" allowed="true" />
+
+ <!-- Enable recording for Peru:
+ The Peruvian Constitution states that people own their own voice and images. If said images
+ or recordings are made for archival purposes, it is allowed. While wiretapping is illegal,
+ it has been used as legal evidence in a court of law. As long as one of the persons talking
+ agrees to the recording, said recording can be used in a court of law. There may be
+ exceptions if the communication contains information that may affect third parties, or if
+ it can be considered as information that should be blocked by medical or legal
+ confidentiality.
+
+ Wiretapping:
+ https://diariouno.pe/columna/chuponeo-prueba-prohibida-o-valida/
+
+ Legal article:
+ https://laley.pe/art/2679/una-grabacion-no-consentida-puede-ser-prueba-de-un-delito-
+
+ Legality of voice recordings and images:
+ https://commons.m.wikimedia.org/wiki/Special:MyLanguage/Commons:Country_specific_consent_requirements#Peru
+ -->
+ <country iso="pe" allowed="true" />
+
+ <!-- Enable recording for Poland:
+ Article 267 of the Polish Penal Code defines call recording as legal for private citizens,
+ when the recording is made by a party to the call.
+
+ Penal Code:
+ https://supertrans2014.files.wordpress.com/2014/06/the-criminal-code.pdf
+
+ Legal articles:
+ https://www.alfatronik.com.pl/info/nagrywanie-rozmow-legalne/
+ https://bezprawnik.pl/legalnosc-nagrywania-rozmowy/
+ -->
+ <country iso="pl" allowed="true" />
+
+ <!-- Disable recording for Puerto Rico:
+ Title Thirty-three of the Penal Code of 2004, Subtitle 5, Special Provisions, Part I:
+ Crimes Against the Person, Chapter 301: Crimes Against Civil Rights, Subchapter II: Crimes
+ Against the Right to Privacy, 33 L.P.R.A § 4809 defines the recording of a private personal
+ conversation, without the express authorization of all parties involved in it, as a
+ misdemeanor. This, in effect, means that recording a phone call as one of the two parties
+ is a criminal offense if done so without the explicit notification and consent of the other
+ party. This territory of the United States conforms with its State Laws. For further
+ information, check 'us'.
+
+ 33 L.P.R.A. § 4809. Recording of communications by a participant:
+ https://bit.ly/2UqbrRC
+ -->
+ <country iso="pr" allowed="false" />
+
+ <!-- Disable recording for Portugal:
+ Privacy is a fundamental right in Portuguese law, as it is defined in Articles 26(1) and 34
+ of the Portuguese Constitution. Infringing on said rights constitutes a crime, as defined
+ in Articles 192(1), 194(2) and 199(1) of the Portuguese Penal Code. The punishment is
+ imprisonment for a period of up to one year or a fine equaling 240 days of pay, either of
+ which may be increased by a third, based on Article 197. Lower courts and Higher courts
+ have been ruling both for and against recording one's own calls, no matter the reason, and
+ there have been numerous cases of exceptions being made, despite what the law says. A
+ complaint has been lodged with the European Court of Human Rights, which may lead to a
+ reversal in the current laws and prohibitions. It is noteworthy that in one case, the
+ Supreme Court rendered a decision, which can be translated as such: "The protection of
+ speech that embodies criminal practices or the image that portrays them must yield to the
+ interest of protecting the victim and the efficiency of criminal justice: protection ends
+ when what is protected is a crime."
+
+ Constitution:
+ https://www.parlamento.pt/Legislacao/Paginas/ConstituicaoRepublicaPortuguesa.aspx
+
+ Penal Code: (nonencrypted link)
+ http://www.pgdlisboa.pt/leis/lei_mostra_articulado.php?artigo_id=109A0199&nid=109&tabela=leis&pagina=1&ficha=1&nversao=
+
+ Examples of privacy as a fundamental right: (nonencrypted link)
+ http://www.dgsi.pt/jtrg.nsf/86c25a698e4e7cb7802579ec004d3832/ab509203321d898d802579ea00576d95?OpenDocument
+ http://www.dgsi.pt/jtre.nsf/134973db04f39bf2802579bf005f080b/be3732dc1664576d8025836100514c19
+
+ Examples of exceptions: (nonencrypted links)
+ https://portal.oa.pt/comunicacao/imprensa/2017/11/12/tribunais-aprovam-videos-de-telemovel-apesar-da-legislacao/
+ http://www.dgsi.pt/jtrp.nsf/-/CC3190F093E769FC80257F69004D9E7B
+ http://www.dgsi.pt/jtrl.nsf/0/44ed8c6ca2d940d580256f250052bfd8
+
+ Complaint to ECHR:
+ https://hudoc.echr.coe.int/eng#{%22itemid%22:[%22001-184193%22]}
+
+ Quoted Supreme Court Case: (nonencrypted link)
+ http://www.dgsi.pt/jstj.nsf/954f0ce6ad9dd8b980256b5f003fa814/25cd7aa80cc3adb0802579260032dd4a?OpenDocument
+
+ Legal alternatives: (nonencrypted links)
+ http://www.dgsi.pt/jtrc.nsf/c3fb530030ea1c61802568d9005cd5bb/c5bb36d9a0470bdd80257b400048f9f2?OpenDocument
+ http://www.dgsi.pt/jtrg.nsf/86c25a698e4e7cb7802579ec004d3832/ff947b8a3fda778780257c0000478b5a
+ -->
+ <country iso="pt" allowed="false" />
+
+ <!-- Enable recording for Romania:
+ The Telecommunications Act (506/2004) states that the recording of a conversation by a
+ party to that conversation is permitted and not a criminal offense. Nevertheless, while
+ such recordings are legal, making use of them may fall subject to further civil or criminal
+ laws. Admissibility as evidence depends on how the recording was obtained.
+
+ Telecommunications Act: (nonencrypted link)
+ http://legislatie.just.ro/Public/DetaliiDocument/56973#id_artA88_ttl
+
+ Civil Procedure Code:
+ https://www.dreptonline.ro/legislatie/codul_procedura_civila_consolidat.php
+
+ Criminal Procedure Code:
+ https://www.dreptonline.ro/legislatie/codul_procedura_penala_2007.php
+
+ Legal article:
+ https://www.dsclex.ro/coduri/cciv2.htm
+ -->
+ <country iso="ro" allowed="true" />
+
+ <!-- Enable recording for Serbia:
+ Article 143 of the Serbian Penal Code covers unauthorized wiretapping and recordings. While
+ it is criminally punishable to share call recordings or wiretap them, the law specifically
+ states recording is only punishable when said recording is 'not meant for him/her', hence
+ it is legal to record your own calls, but not to share them with third parties.
+
+ Serbian Penal Code:
+ https://www.paragraf.rs/propisi/krivicni_zakonik.html
+ -->
+ <country iso="rs" allowed="true" />
+
+ <!-- Enable recording for Russia:
+ Recording a phone call when not one of the two parties participating in said call is
+ illegal and punishable by law. As a party to a phone call, one may record it without
+ notifying the other side, as is evident in the decision of the Supreme Court of Russia for
+ case 35-KG16-18, which was rendered on the 6th of December 2016. This concerns civil cases
+ between two private citizens. Whether this covers cases involving legal entities or people
+ holding a public position has not been researched. The key laws to consider are the Federal
+ Law of 27th July 2006, N 149-FZ, (amended on 18th March 2019) "Information, Information
+ Technologies and Information Security" - Article 9, Subarticle 8, as well as the Civil
+ Procedure Code of 2002, N 138-FZ (amended on the 27th December 2018), article 55. Citations
+ of other pertinent laws may be found in the linked decision, starting at internal document
+ page number 4.
+
+ Supreme Court of Russia, Decision on case number 35-KG16-18: (nonencrypted link)
+ http://www.supcourt.ru/stor_pdf.php?id=1502686
+
+ Federal Law of 27th July 2006, N 149-FZ:
+ https://www.consultant.ru/document/cons_doc_LAW_61798/35f4fb38534799919febebd589466c9838f571b2/
+
+ Civil Procedure Code of 2002, N 138-FZ:
+ https://www.consultant.ru/document/cons_doc_LAW_39570/b48406042a309ee368f395fb6f3be1d43c7cbfc2/
+ -->
+ <country iso="ru" allowed="true" />
+
+ <!-- Enable recording for Sweden:
+ According to the Swedish Penal Code (Brottsbalken), Chapter 4, 8–9 §§, it is illegal to
+ make unauthorized recordings of telephone conversations as a third party. A court can grant
+ permission for law enforcement agencies to tap telephone lines. Anyone participating in the
+ telephone call may record the conversation. A recording is always admissible as evidence in
+ a court of law, even when obtained in an illegal way.
+
+ Criminal Code: (nonencrypted link)
+ https://lagen.nu/begrepp/Olovlig_avlyssning
+ http://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/brottsbalk-1962700_sfs-1962-700
+ -->
+ <country iso="se" allowed="true" />
+
+ <!-- Enable recording for Singapore:
+ Singaporean law does not recognize privacy as a right that can be infringed upon. A party
+ can not be prosecuted or sued for recording a conversation he or she is a part of. The only
+ exception is when said recording contains confidential information, in which case the party
+ may or may not be liable for their actions, if said party makes use of said confidential
+ information in a way that clearly brings him or her gains of any sort, and/or harms the
+ other party in any perceivable way.
+
+ Legal discussion:
+ https://singaporelegaladvice.com/can-i-record-a-conversation-without-consent/
+
+ Personal Data Protection Act 2012:
+ https://sso.agc.gov.sg/Act/PDPA2012
+ https://www.pdpc.gov.sg/Legislation-and-Guidelines/Personal-Data-Protection-Act-Overview
+
+ Copyright Act, Revised Edition 2006:
+ https://sso.agc.gov.sg/Act/CA1987
+ -->
+ <country iso="sg" allowed="true" />
+
+ <!-- Enable recording for Slovenia:
+ Article 148 of the Slovenian Criminal Code covers the unlawful eavesdropping and sound
+ recording. Subarticle 1 defines a maximum punishment of no more than one year for the
+ unlawful eavesdropping or recording of a private conversation by use of special devices,
+ or directly transmitting said conversation to a third person. This also includes passing on
+ the gist of said conversation. Subarticle 2 states that recording another person's
+ statement with the intent to misuse it, without his or her consent, is punishable in the
+ way postulated in Subarticle 1. Prosecution is initiated by the aggrieved party for
+ Subarticle 1, while under Subarticle 2 it is initiated upon a private action. Based on
+ Article 148, it is legal to record one's own calls, when not done so with the intent to
+ misuse said recordings. Sharing said recordings in any way may be deemed a criminal or
+ civil offense.
+
+ Criminal Code:
+ https://www.wipo.int/edocs/lexdocs/laws/en/si/si046en.pdf
+ -->
+ <country iso="si" allowed="true" />
+
+ <!-- Enable recording for Slovakia:
+ Sections 376 and 377 of the Slovakian Criminal Code cover breach of confidentiality of
+ spoken utterance and other forms of personal expression and the breach of secrecy of all
+ types of instruments, recordings and documents. Section 376 states that the breach of
+ secrecy, by way of disclosing or making available to a third party and/or using it to cause
+ serious harm to another party, leads to an imprisonment of up to two years. Section 377
+ defines breach of confidentiality as the making of an unlawful recording accessible to a
+ third person or using it in any way that would hinder the other side's rights. This is
+ punishable with a term of imprisonment of up to two years.
+
+ Slovakian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/4/Slovakia/show
+ -->
+ <country iso="sk" allowed="true" />
+
+ <!-- Enable recording for Turkey:
+ Article 132 of Law 5237, the Turkish Penal Code, sets the punishment for violating the
+ secrecy of communication as six months to two years of imprisonment. If the violation of
+ secrecy is done in the form of a recording, then the punishment is imprisonment of one to
+ three years. The act of unlawfully publishing the contents of a communication is
+ imprisonment of one to three years. Openly disclosing the content of a communication
+ between oneself and others, without the other party or parties' consent, is imprisonment of
+ six months to two years. If disclosure is done by way of the press or broadcast, the
+ punishment is increased by one half. One may listen and record conversations of other
+ parties, with the consent of at least one party. Doing so without consent is punishable
+ with imprisonment of two to six months. Article 135 of the Turkish Penal Code defines
+ punishment in the case of recording personal data and information. It stands to reason that
+ one may record one's own calls without the consent of the other party, but the act of
+ sharing those recordings in any way may result in persecution, either in the form of a
+ criminal or civil case against the party that has recorded his or hew own calls.
+
+ Turkish Penal Code:
+ https://www.wipo.int/edocs/lexdocs/laws/en/tr/tr171en.pdf
+ -->
+ <country iso="tr" allowed="true" />
+
+ <!-- Enable recording for Ukraine:
+ Sharing a call recording without consent is a punishable offense, and can not be used as
+ valid proof in a court of law. Call recordings may be handed over to the authorities, by
+ one of the two parties without the other party's consent, when a crime is mentioned in the
+ recording. In such cases the party that handed over the recording is not liable to fines or
+ punishment, as the authorities will use the recording to initiate an investigation, but not
+ as proof of a crime. The above information is covered in Articles 31 and 32 of the
+ Constitution, as well as Articles 163, 182 and 359 of the Criminal Code.
+
+ Constitution of Ukraine:
+ https://zakon.rada.gov.ua/cgi-bin/laws/main.cgi?nreg=254%EA%2F96%2D%E2%F0
+
+ Ukrainian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/52
+
+ Radio Svoboda legal advice page:
+ https://www.radiosvoboda.org/a/details/28905674.html
+
+ Legal discussion:
+ https://sklaw.com.ua/ua/news/345_pro_naslidki_zapisu_telefonnoi_rozmovi_rozpovila_advokat_ao_spenser
+ -->
+ <country iso="ua" allowed="true" />
+
+ <!-- Disable recording for United States of America, Guam, the Northern Mariana Islands, the
+ United States Virgin Islands, Puerto Rico:
+ Currently federal laws state that call recording is legal. On the other hand, each state
+ has its own laws which take priority. Most states allow call recording when one sides
+ agrees, but over 10 require both sides to agree. Since there are no ISO country codes per
+ state, there is no way to differentiate whether the state you are currently in allows call
+ recording or not. Due to this, call recording is set as disabled for the United States of
+ America and some of its territories. These include Guam, the Northern Mariana Islands, the
+ United States Virgin Islands and Puerto Rico. Until a method for properly differentiating
+ between states is created, or a law or precedent emerges which would allow call recording
+ to be legal in all of the USA and its territories, all aforementioned countries and
+ territories should be set as false. Even if such a method is to be found, there is still
+ the question of Native American Reservations, territories that have a cumulative size of
+ over 200 000 square kilometers and might enforce their own set of laws for call recording.
+
+ Two-party consent state laws:
+ https://recordinglaw.com/party-two-party-consent-states/
+ -->
+ <country iso="us,vi,gu" allowed="false" />
+
+ <!-- Enable recording for Kosovo:
+ Article 36 of the Constitution of Kosovo defines the Right to Privacy. Article 202 of the
+ Criminal Code of Kosovo covers the infringing of privacy in corresepondence and computer
+ databases, which can be best explained as the act of violating and/or sharing a private
+ document with another person. Article 203 covers the unauthorized disclosure of
+ confidential information, when the person disclosing said information is under legal duty
+ to maintain it as confidential. Said person is not liable when the disclosure of the
+ confidential information is done so in the interest of the public. Paragraph 4 describes
+ public interest as the welfare of the general public outweighing the individual interest.
+ It is also permissible to use this as a defense when the disclosed information involves
+ plans, preparation or the commission of crimes against the constitutional order or
+ territorial integrity of the Republic of Kosovo or other criminal offenses that will cause
+ great bodily injury or death to another person. Article 204 covers the unauthorized
+ interception of a conversation or statement. Article 205 covers the unauthorized
+ photographing or video recording of a person 'in his or her personal premises or in any
+ other place where a person has a reasonable expectation of privacy', with paragraph 4
+ offering an exception for liability when the act is done to discover a criminal offence or
+ the perpetrators of a criminal offence, or to present as evidence to the police,
+ prosecution or court, and if the photos or recordings are submitted to these authorities.
+ It is beyond doubt that none of these articles deal with recording personal conversation
+ between two parties, hence the act of doing so is not explicitly punishable and is not a
+ criminal offense that would warrant a criminal case.
+
+ Constitution of Kosovo:
+ https://kuvendikosoves.org/?cid=2,1058
+ Criminal Code of Kosovo:
+ https://assembly-kosova.org/common/docs/ligjet/Criminal%20Code.pdf
+ -->
+ <country iso="xk" allowed="true" />
+
+ <!-- Enable recording for South Africa:
+ Under the Regulation of Interception of Communications and Provision of
+ Communication-related Information Act of 2003, 4(1)(a)(b) as well as 16(5)(a)(b), it is
+ legal for a party of a conversation to record said conversation, when there are reasonable
+ grounds to believe that said act will prevent a crime, prevent bodily harm, is in the
+ interest of public safety or one of the other reasons stated in the previously noted
+ paragraphs.
+
+ Regulation of Interception of Communications Act of 2003:
+ https://www.gov.za/sites/default/files/gcis_document/201409/a70-02.pdf
+ -->
+ <country iso="za" allowed="true" />
+
+</call-record-allowed-flags>
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();
diff --git a/privapp_whitelist_com.android.dialer-ext.xml b/privapp_whitelist_com.android.dialer-ext.xml
new file mode 100644
index 000000000..09e6e0fda
--- /dev/null
+++ b/privapp_whitelist_com.android.dialer-ext.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019-2020 The LineageOS 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.
+-->
+<permissions>
+ <!-- Additional permissions on top of privapp_whitelist_com.android.dialer.xml -->
+ <privapp-permissions package="com.android.dialer">
+ <permission name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
+ </privapp-permissions>
+</permissions>