diff options
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> |