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